diff options
author | Hygna <hygna@proton.me> | 2022-10-25 12:43:59 +0100 |
---|---|---|
committer | Hygna <hygna@proton.me> | 2022-10-25 12:43:59 +0100 |
commit | 65243b5b9354034d2d46cbf69dfa4d96b0a76632 (patch) | |
tree | 4d7d1bd551d4350207633ef0d9db36fd0c79ef81 /src/assets | |
parent | Stopped tracking generated html files in git (diff) | |
download | libredirect-65243b5b9354034d2d46cbf69dfa4d96b0a76632.zip |
Improved the instance fetcher
Changed the image used in CI Started fetching Whoogle & SimplyTranslate tor & i2p instances Started using a custom user agent for transparency
Diffstat (limited to 'src/assets')
-rw-r--r-- | src/assets/javascripts/services.js | 1702 |
1 files changed, 851 insertions, 851 deletions
diff --git a/src/assets/javascripts/services.js b/src/assets/javascripts/services.js index d02e03f6..e67ed842 100644 --- a/src/assets/javascripts/services.js +++ b/src/assets/javascripts/services.js @@ -1,851 +1,851 @@ -window.browser = window.browser || window.chrome - -import utils from "./utils.js" - -let config, options, redirects, targets - -function init() { - return new Promise(async resolve => { - browser.storage.local.get(["options", "redirects", "targets"], r => { - options = r.options - redirects = r.redirects - targets = r.targets - fetch("/config/config.json") - .then(response => response.text()) - .then(configData => { - config = JSON.parse(configData) - resolve() - }) - }) - }) -} - -init() -browser.storage.onChanged.addListener(init) - -function fetchFrontendInstanceList(service, frontend, redirects, options, config) { - let tmp = [] - if (config.services[service].frontends[frontend].instanceList) { - for (const network in config.networks) { - tmp.push(...redirects[network], ...options[frontend][network].custom) - } - } else if (config.services[service].frontends[frontend].singleInstance) tmp = config.services[service].frontends[frontend].singleInstance - return tmp -} - -function all(service, frontend, options, config, redirects) { - let instances = [] - if (!frontend) { - for (const frontend in config.services[service].frontends) { - instances.push(...fetchFrontendInstanceList(service, frontend, redirects[frontend], options, config)) - } - } else { - instances.push(...fetchFrontendInstanceList(service, frontend, redirects[frontend], options, config)) - } - return instances -} - -function regexArray(service, url, config) { - if (config.services[service].targets == "datajson") { - if (targets[service].includes(utils.protocolHost(url))) return true - } else { - const targetList = config.services[service].targets - for (const targetString in targetList) { - const target = new RegExp(targetList[targetString]) - if (target.test(url.href)) return true - } - } - return false -} - -function redirect(url, type, initiator, forceRedirection) { - if (type != "main_frame" && type != "sub_frame") return - let randomInstance - let frontend - for (const service in config.services) { - if (!forceRedirection && !options[service].enabled) continue - if (config.services[service].embeddable && type != options[service].redirectType && options[service].redirectType != "both") continue - if (!config.services[service].embeddable && type != "main_frame") continue - // let targets = new RegExp(config.services[service].targets.join("|"), "i") - - if (!regexArray(service, url, config)) continue - // if (initiator) { - // console.log(initiator.host) - // if (targets.test(initiator.host)) continue - // //if (all(service, null, options, config, redirects).includes(initiator.origin) && reverse(initiator) == url) return "BYPASSTAB" - // } - - if (Object.keys(config.services[service].frontends).length > 1) { - if (type == "sub_frame" && config.services[service].embeddable && !config.services[service].frontends[options[service].frontend].embeddable) frontend = options[service].embedFrontend - else frontend = options[service].frontend - } else frontend = Object.keys(config.services[service].frontends)[0] - - if (config.services[service].frontends[frontend].instanceList) { - let instanceList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom] - if (instanceList.length === 0 && options.networkFallback) instanceList = [...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom] - if (instanceList.length === 0) return - randomInstance = utils.getRandomInstance(instanceList) - } else if (config.services[service].frontends[frontend].singleInstance) randomInstance = config.services[service].frontends[frontend].singleInstance - break - } - if (!frontend) return - - // Here is a (temperory) space for defining constants required in 2 or more switch cases. - // When possible, try have the two switch cases share all their code as done with searx and searxng. - // Do not do that when they do not share 100% of their code. - - const mapCentreRegex = /@(-?\d[0-9.]*),(-?\d[0-9.]*),(\d{1,2})[.z]/ - const dataLatLngRegex = /!3d(-?[0-9]{1,}.[0-9]{1,})!4d(-?[0-9]{1,}.[0-9]{1,})/ - const placeRegex = /\/place\/(.*)\// - function convertMapCentre() { - let [lat, lon, zoom] = [null, null, null] - if (url.pathname.match(mapCentreRegex)) { - // Set map centre if present - ;[lat, lon, zoom] = url.pathname.match(mapCentreRegex) - } else if (url.searchParams.has("center")) { - ;[lat, lon] = url.searchParams.get("center").split(",") - zoom = url.searchParams.get("zoom") ?? "17" - } - return [zoom, lon, lat] - } - - switch (frontend) { - // This is where all instance-specific code must be ran to convert the service url to one that can be understood by the frontend. - case "beatbump": - return `${randomInstance}${url.pathname}${url.search}` - .replace("/watch?v=", "/listen?id=") - .replace("/channel/", "/artist/") - .replace("/playlist?list=", "/playlist/VL") - .replace(/\/search\?q=.*/, searchQuery => searchQuery.replace("?q=", "/") + "?filter=all") - case "hyperpipe": - return `${randomInstance}${url.pathname}${url.search}`.replace(/\/search\?q=.*/, searchQuery => searchQuery.replace("?q=", "/")) - case "bibliogram": - const reservedPaths = ["u", "p", "privacy"] - if (url.pathname === "/" || reservedPaths.includes(url.pathname.split("/")[1])) return `${randomInstance}${url.pathname}${url.search}` - if (url.pathname.startsWith("/reel") || url.pathname.startsWith("/tv")) return `${randomInstance}/p${url.pathname.replace(/\/reel|\/tv/i, "")}${url.search}` - else return `${randomInstance}/u${url.pathname}${url.search}` // Likely a user profile, redirect to '/u/...' - case "lbryDesktop": - return url.href.replace(/^https?:\/{2}odysee\.com\//, "lbry://").replace(/:(?=[a-zA-Z0-9])/g, "#") - case "neuters": - if (url.pathname.startsWith("/article/") || url.pathname.startsWith("/pf/") || url.pathname.startsWith("/arc/") || url.pathname.startsWith("/resizer/")) return null - else if (url.pathname.endsWith("/")) return `${randomInstance}${url.pathname}` - else return `${randomInstance}${url.pathname}/` - case "searx": - case "searxng": - return `${randomInstance}/?q=${encodeURIComponent(url.searchParams.get("q"))}` - case "whoogle": - return `${randomInstance}/search?q=${encodeURIComponent(url.searchParams.get("q"))}` - case "librex": - return `${randomInstance}/search.php?q=${encodeURIComponent(url.searchParams.get("q"))}` - case "send": - return randomInstance - case "nitter": - let search = new URLSearchParams(url.search) - - search.delete("ref_src") - search.delete("ref_url") - - search = search.toString() - if (search !== "") search = `?${search}` - - if (url.host.split(".")[0] === "pbs" || url.host.split(".")[0] === "video") { - try { - const [, id, format, extra] = search.match(/(.*)\?format=(.*)&(.*)/) - const query = encodeURIComponent(`${id}.${format}?${extra}`) - return `${randomInstance}/pic${url.pathname}${query}` - } catch { - return `${randomInstance}/pic${url.pathname}${search}` - } - } - - if (url.pathname.split("/").includes("tweets")) return `${randomInstance}${url.pathname.replace("/tweets", "")}${search}` - if (url.host == "t.co") return `${randomInstance}/t.co${url.pathname}` - return `${randomInstance}${url.pathname}${search}` - case "yattee": - return url.href.replace(/^https?:\/{2}/, "yattee://") - case "freetube": - return `freetube://https://youtu.be${url.pathname}${url.search}`.replace(/watch\?v=/, "") - case "simplyTranslate": - return `${randomInstance}/${url.search}` - case "libreTranslate": - return `${randomInstance}/${url.search}` - .replace(/(?<=\/?)sl/, "source") - .replace(/(?<=&)tl/, "target") - .replace(/(?<=&)text/, "q") - case "osm": { - if (initiator && initiator.host === "earth.google.com") return - const travelModes = { - driving: "fossgis_osrm_car", - walking: "fossgis_osrm_foot", - bicycling: "fossgis_osrm_bike", - transit: "fossgis_osrm_car", // not implemented on OSM, default to car. - } - - function addressToLatLng(address) { - const xmlhttp = new XMLHttpRequest() - xmlhttp.open("GET", `https://nominatim.openstreetmap.org/search/${address}?format=json&limit=1`, false) - xmlhttp.send() - if (xmlhttp.status === 200) { - const json = JSON.parse(xmlhttp.responseText)[0] - if (json) { - console.log("json", json) - return [`${json.lat},${json.lon}`, `${json.boundingbox[2]},${json.boundingbox[1]},${json.boundingbox[3]},${json.boundingbox[0]}`] - } - } - console.info("Error: Status is " + xmlhttp.status) - } - - let mapCentre = "#" - let prefs = {} - - const mapCentreData = convertMapCentre() - if (mapCentreData[0] && mapCentreData[1] && mapCentreData[2]) mapCentre = `#map=${mapCentreData[0]}/${mapCentreData[1]}/${mapCentreData[2]}` - if (url.searchParams.get("layer")) prefs.layers = osmLayers[url.searchParams.get("layer")] - - if (url.pathname.includes("/embed")) { - // Handle Google Maps Embed API - // https://www.google.com/maps/embed/v1/place?key=AIzaSyD4iE2xVSpkLLOXoyqT-RuPwURN3ddScAI&q=Eiffel+Tower,Paris+France - //console.log("embed life") - - let query = "" - if (url.searchParams.has("q")) query = url.searchParams.get("q") - else if (url.searchParams.has("query")) query = url.searchParams.has("query") - else if (url.searchParams.has("pb")) - try { - query = url.searchParams.get("pb").split(/!2s(.*?)!/)[1] - } catch (error) { - console.error(error) - } // Unable to find map marker in URL. - - let [coords, boundingbox] = addressToLatLng(query) - prefs.bbox = boundingbox - prefs.marker = coords - prefs.layer = "mapnik" - let prefsEncoded = new URLSearchParams(prefs).toString() - return `${randomInstance}/export/embed.html?${prefsEncoded}` - } else if (url.pathname.includes("/dir")) { - // Handle Google Maps Directions - // https://www.google.com/maps/dir/?api=1&origin=Space+Needle+Seattle+WA&destination=Pike+Place+Market+Seattle+WA&travelmode=bicycling - - let travMod = url.searchParams.get("travelmode") - if (url.searchParams.has("travelmode")) prefs.engine = travelModes[travMod] - - let orgVal = url.searchParams.get("origin") - let destVal = url.searchParams.get("destination") - - let org = addressToLatLng(orgVal) - let dest = addressToLatLng(destVal) - prefs.route = `${org};${dest}` - - let prefsEncoded = new URLSearchParams(prefs).toString() - return `${randomInstance}/directions?${prefsEncoded}${mapCentre}` - } else if (url.pathname.includes("data=") && url.pathname.match(dataLatLngRegex)) { - // Get marker from data attribute - // https://www.google.com/maps/place/41%C2%B001'58.2%22N+40%C2%B029'18.2%22E/@41.032833,40.4862063,17z/data=!3m1!4b1!4m6!3m5!1s0x0:0xf64286eaf72fc49d!7e2!8m2!3d41.0328329!4d40.4883948 - //console.log("data life") - - let [, mlat, mlon] = url.pathname.match(dataLatLngRegex) - - return `${randomInstance}/search?query=${mlat}%2C${mlon}` - } else if (url.searchParams.has("ll")) { - // Get marker from ll param - // https://maps.google.com/?ll=38.882147,-76.99017 - //console.log("ll life") - - const [mlat, mlon] = url.searchParams.get("ll").split(",") - - return `${randomInstance}/search?query=${mlat}%2C${mlon}` - } else if (url.searchParams.has("viewpoint")) { - // Get marker from viewpoint param. - // https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=48.857832,2.295226&heading=-45&pitch=38&fov=80 - //console.log("viewpoint life") - - const [mlat, mlon] = url.searchParams.get("viewpoint").split(",") - - return `${randomInstance}/search?query=${mlat}%2C${mlon}` - } else { - // Use query as search if present. - //console.log("normal life") - - let query - if (url.searchParams.has("q")) query = url.searchParams.get("q") - else if (url.searchParams.has("query")) query = url.searchParams.get("query") - else if (url.pathname.match(placeRegex)) query = url.pathname.match(placeRegex)[1] - - let prefsEncoded = new URLSearchParams(prefs).toString() - if (query) return `${randomInstance}/search?query="${query}${mapCentre}&${prefsEncoded}` - } - - let prefsEncoded = new URLSearchParams(prefs).toString() - console.log("mapCentre", mapCentre) - console.log("prefs", prefs) - console.log("prefsEncoded", prefsEncoded) - return `${randomInstance}/${mapCentre}&${prefsEncoded}` - } - case "facil": { - if (initiator && initiator.host === "earth.google.com") return - const travelModes = { - driving: "car", - walking: "pedestrian", - bicycling: "bicycle", - transit: "car", // not implemented on Facil, default to car. - } - const mapCentreData = convertMapCentre() - let mapCentre = "#" - if (mapCentreData[0] && mapCentreData[1] && mapCentreData[2]) mapCentre = `#${mapCentreData[0]}/${mapCentreData[1]}/${mapCentreData[2]}` - - if (url.pathname.includes("/embed")) { - // Handle Google Maps Embed API - // https://www.google.com/maps/embed/v1/place?key=AIzaSyD4iE2xVSpkLLOXoyqT-RuPwURN3ddScAI&q=Eiffel+Tower,Paris+France - //console.log("embed life") - - let query = "" - if (url.searchParams.has("q")) query = url.searchParams.get("q") - else if (url.searchParams.has("query")) query = url.searchParams.has("query") - else if (url.searchParams.has("pb")) - try { - query = url.searchParams.get("pb").split(/!2s(.*?)!/)[1] - } catch (error) { - console.error(error) - } // Unable to find map marker in URL. - - return `${randomInstance}/#q=${query}` - } else if (url.pathname.includes("/dir")) { - // Handle Google Maps Directions - // https://www.google.com/maps/dir/?api=1&origin=Space+Needle+Seattle+WA&destination=Pike+Place+Market+Seattle+WA&travelmode=bicycling - - let travMod = url.searchParams.get("travelmode") - - let orgVal = url.searchParams.get("origin") - let destVal = url.searchParams.get("destination") - - return `${randomInstance}/#q=${orgVal}%20to%20${destVal}%20by%20${travelModes[travMod]}` - } else if (url.pathname.includes("data=") && url.pathname.match(dataLatLngRegex)) { - // Get marker from data attribute - // https://www.google.com/maps/place/41%C2%B001'58.2%22N+40%C2%B029'18.2%22E/@41.032833,40.4862063,17z/data=!3m1!4b1!4m6!3m5!1s0x0:0xf64286eaf72fc49d!7e2!8m2!3d41.0328329!4d40.4883948 - //console.log("data life") - - let [, mlat, mlon] = url.pathname.match(dataLatLngRegex) - - return `${randomInstance}/#q=${mlat}%2C${mlon}` - } else if (url.searchParams.has("ll")) { - // Get marker from ll param - // https://maps.google.com/?ll=38.882147,-76.99017 - //console.log("ll life") - - const [mlat, mlon] = url.searchParams.get("ll").split(",") - - return `${randomInstance}/#q=${mlat}%2C${mlon}` - } else if (url.searchParams.has("viewpoint")) { - // Get marker from viewpoint param. - // https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=48.857832,2.295226&heading=-45&pitch=38&fov=80 - //console.log("viewpoint life") - - const [mlat, mlon] = url.searchParams.get("viewpoint").split(",") - - return `${randomInstance}/#q=${mlat}%2C${mlon}` - } else { - // Use query as search if present. - //console.log("normal life") - - let query - if (url.searchParams.has("q")) query = url.searchParams.get("q") - else if (url.searchParams.has("query")) query = url.searchParams.get("query") - else if (url.pathname.match(placeRegex)) query = url.pathname.match(placeRegex)[1] - - if (query) return `${randomInstance}/${mapCentre}/Mpnk/${query}` - } - } - case "wikiless": - let GETArguments = [] - if (url.search.length > 0) { - let search = url.search.substring(1) //get rid of '?' - let argstrings = search.split("&") - for (let i = 0; i < argstrings.length; i++) { - let args = argstrings[i].split("=") - GETArguments.push([args[0], args[1]]) - } - } - - let link = `${randomInstance}${url.pathname}` - let urlSplit = url.host.split(".") - if (urlSplit[0] != "wikipedia" && urlSplit[0] != "www") { - if (urlSplit[0] == "m") GETArguments.push(["mobileaction", "toggle_view_mobile"]) - else GETArguments.push(["lang", urlSplit[0]]) - if (urlSplit[1] == "m") GETArguments.push(["mobileaction", "toggle_view_mobile"]) - // wikiless doesn't have mobile view support yet - } - for (let i = 0; i < GETArguments.length; i++) link += (i == 0 ? "?" : "&") + GETArguments[i][0] + "=" + GETArguments[i][1] - return link - - case "lingva": - let params_arr = url.search.split("&") - params_arr[0] = params_arr[0].substring(1) - let params = {} - for (let i = 0; i < params_arr.length; i++) { - let pair = params_arr[i].split("=") - params[pair[0]] = pair[1] - } - if (params.sl && params.tl && params.text) { - return `${randomInstance}/${params.sl}/${params.tl}/${params.text}` - } - return randomInstance - case "breezeWiki": - let wiki = url.hostname.match(/^[a-zA-Z0-9-]+(?=\.fandom\.com)/) - if (wiki == "www" || !wiki) wiki = "" - else wiki = "/" + wiki - if (url.href.search(/Special:Search\?query/) > -1) return `${randomInstance}${wiki}${url.pathname}${url.search}`.replace(/Special:Search\?query/, "search?q").replace(/\/wiki/, "") - else return `${randomInstance}${wiki}${url.pathname}${url.search}` - case "rimgo": - if (url.href.search(/^https?:\/{2}(?:[im]\.)?stack\./) > -1) return `${randomInstance}/stack${url.pathname}${url.search}` - else return `${randomInstance}${url.pathname}${url.search}` - case "libreddit": - const subdomain = url.hostname.match(/^(?:(?:external-)?preview|i)(?=\.redd\.it)/) - if (!subdomain) return `${randomInstance}${url.pathname}${url.search}` - switch (subdomain[0]) { - case "preview": - return `${randomInstance}/preview/pre${url.pathname}${url.search}` - case "external-preview": - return `${randomInstance}/preview/external-pre${url.pathname}${url.search}` - case "i": - return `${randomInstance}/img${url.pathname}` - } - case "teddit": - if (/^(?:(?:external-)?preview|i)\.redd\.it/.test(url.hostname)) { - if (url.search == "") return `${randomInstance}${url.pathname}?teddit_proxy=${url.hostname}` - else return `${randomInstance}${url.pathname}${url.search}&teddit_proxy=${url.hostname}` - } - return `${randomInstance}${url.pathname}${url.search}` - default: - return `${randomInstance}${url.pathname}${url.search}` - } -} - -function computeService(url, returnFrontend) { - return new Promise(resolve => { - fetch("/config/config.json") - .then(response => response.text()) - .then(configData => { - const config = JSON.parse(configData) - browser.storage.local.get(["redirects", "options"], r => { - const redirects = r.redirects - const options = r.options - for (const service in config.services) { - if (regexArray(service, url, config)) { - resolve(service) - return - } else { - for (const frontend in config.services[service].frontends) { - if (all(service, frontend, options, config, redirects).includes(utils.protocolHost(url))) { - if (returnFrontend) resolve([service, frontend, utils.protocolHost(url)]) - else resolve(service) - return - } - } - } - } - resolve() - }) - }) - }) -} - -function switchInstance(url) { - return new Promise(async resolve => { - await init() - const protocolHost = utils.protocolHost(url) - for (const service in config.services) { - if (!all(service, null, options, config, redirects).includes(protocolHost)) continue - - let instancesList - if (Object.keys(config.services[service].frontends).length == 1) { - const frontend = Object.keys(config.services[service].frontends)[0] - instancesList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom] - if (instancesList.length === 0 && options.networkFallback) instancesList = [...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom] - } else { - const frontend = options[service].frontend - instancesList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom] - if (instancesList.length === 0 && options.networkFallback) instancesList = [...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom] - } - - let oldInstance - const i = instancesList.indexOf(protocolHost) - if (i > -1) { - oldInstance = instancesList[i] - instancesList.splice(i, 1) - } - if (instancesList.length === 0) { - resolve() - return - } - const randomInstance = utils.getRandomInstance(instancesList) - const oldUrl = `${oldInstance}${url.pathname}${url.search}` - // This is to make instance switching work when the instance depends on the pathname, eg https://darmarit.org/searx - // Doesn't work because of .includes array method, not a top priotiry atm - resolve(oldUrl.replace(oldInstance, randomInstance)) - return - } - resolve() - }) -} - -function reverse(url, urlString) { - return new Promise(async resolve => { - await init() - let protocolHost - if (!urlString) protocolHost = utils.protocolHost(url) - else protocolHost = url.match(/https?:\/{2}(?:[^\s\/]+\.)+[a-zA-Z0-9]+/)[0] - for (const service in config.services) { - if (!all(service, null, options, config, redirects).includes(protocolHost)) continue - - switch (service) { - case "instagram": - case "youtube": - case "imdb": - case "imgur": - case "tiktok": - case "twitter": - case "reddit": - case "imdb": - case "reuters": - case "quora": - case "medium": - if (!urlString) resolve(config.services[service].url + url.pathname + url.search) - else resolve(url.replace(/https?:\/{2}(?:[^\s\/]+\.)+[a-zA-Z0-9]+/, config.services[service].url)) - return - default: - resolve() - return - } - } - resolve() - }) -} - -function unifyPreferences(url, tabId) { - return new Promise(async resolve => { - await init() - const protocolHost = utils.protocolHost(url) - for (const service in config.services) { - for (const frontend in config.services[service].frontends) { - if (all(service, frontend, options, config, redirects).includes(protocolHost)) { - let instancesList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom] - if (options.networkFallback && options.network != "clearnet") instancesList.push(...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom) - - const frontendObject = config.services[service].frontends[frontend] - if ("cookies" in frontendObject.preferences) { - for (const cookie of frontendObject.preferences.cookies) { - await utils.copyCookie(url, instancesList, cookie) - } - } - if ("localstorage" in frontendObject.preferences) { - browser.storage.local.set({ tmp: [frontend, frontendObject.preferences.localstorage] }) - browser.tabs.executeScript(tabId, { - file: "/assets/javascripts/get-localstorage.js", - runAt: "document_start", - }) - for (const instance of instancesList) - browser.tabs.create({ url: instance }, tab => - browser.tabs.executeScript(tab.id, { - file: "/assets/javascripts/set-localstorage.js", - runAt: "document_start", - }) - ) - } - /* - if ("indexeddb" in frontendObject.preferences) { - } - if ("token" in frontendObject.preferences) { - } - */ - resolve(true) - return - } - } - } - }) -} - -function setRedirects(passedRedirects) { - return new Promise(resolve => { - fetch("/config/config.json") - .then(response => response.text()) - .then(configData => { - browser.storage.local.get(/* [ */ "options" /* , "blacklists"] */, async r => { - let redirects = passedRedirects - let options = r.options - const config = JSON.parse(configData) - let targets = {} - for (const service in config.services) { - if (config.services[service].targets == "datajson") { - targets[service] = redirects[service] - delete redirects[service] - } - for (const frontend in config.services[service].frontends) { - if (config.services[service].frontends[frontend].instanceList) { - for (const network in config.networks) { - for (const instance of options[frontend][network].enabled) { - let i = redirects[frontend][network].indexOf(instance) - if (i < 0) options[frontend][network].enabled.splice(i, 1) - } - } - } - } - /* - for (const frontend in config.services[service].frontends) { - if (config.services[service].frontends[frontend].instanceList) { - for (const network in config.networks) { - options[frontend][network].enabled = redirects[frontend][network] - } - for (const blacklist in r.blacklists) { - for (const instance of blacklist) { - let i = options[frontend].clearnet.enabled.indexOf(instance) - if (i > -1) options[frontend].clearnet.enabled.splice(i, 1) - } - } - } - } - */ - // The above will be implemented with https://github.com/libredirect/libredirect/issues/334 - } - for (const frontend in redirects) { - let exists = false - for (const service in config.services) if (config.services[service].frontends[frontend]) exists = true - if (!exists) delete redirects[frontend] - else for (const network in redirects[frontend]) if (!config.networks[network]) delete redirects[frontend][network] - } - browser.storage.local.set({ redirects, targets, options }, () => resolve()) - }) - }) - }) -} - -function initDefaults() { - return new Promise(resolve => { - fetch("/instances/data.json") - .then(response => response.text()) - .then(data => { - fetch("/config/config.json") - .then(response => response.text()) - .then(configData => { - browser.storage.local.get(["options", "blacklists"], r => { - let redirects = JSON.parse(data) - let options = r.options - let targets = {} - let config = JSON.parse(configData) - const localstorage = {} - const latency = {} - for (const service in config.services) { - options[service] = {} - if (config.services[service].targets == "datajson") { - targets[service] = redirects[service] - delete redirects[service] - } - for (const defaultOption in config.services[service].options) options[service][defaultOption] = config.services[service].options[defaultOption] - for (const frontend in config.services[service].frontends) { - if (config.services[service].frontends[frontend].instanceList) { - options[frontend] = {} - for (const network in config.networks) { - options[frontend][network] = {} - options[frontend][network].enabled = JSON.parse(data)[frontend][network] - options[frontend][network].custom = [] - } - for (const blacklist in r.blacklists) { - for (const instance of r.blacklists[blacklist]) { - let i = options[frontend].clearnet.enabled.indexOf(instance) - if (i > -1) options[frontend].clearnet.enabled.splice(i, 1) - } - } - } - } - } - browser.storage.local.set({ redirects, options, targets, latency, localstorage }) - resolve() - }) - }) - }) - }) -} - -function upgradeOptions() { - return new Promise(resolve => { - fetch("/config/config.json") - .then(response => response.text()) - .then(configData => { - browser.storage.local.get(null, r => { - let options = r.options - let latency = {} - const config = JSON.parse(configData) - options.exceptions = r.exceptions - if (r.theme != "DEFAULT") options.theme = r.theme - options.popupServices = r.popupFrontends - let tmp = options.popupServices.indexOf("tikTok") - if (tmp > -1) { - options.popupServices.splice(tmp, 1) - options.popupServices.push("tiktok") - } - tmp = options.popupServices.indexOf("sendTarget") - if (tmp > -1) { - options.popupServices.splice(tmp, 1) - options.popupServices.push("sendFiles") - } - options.autoRedirect = r.autoRedirect - switch (r.onlyEmbeddedVideo) { - case "onlyNotEmbedded": - options.youtube.redirectType = "main_frame" - case "onlyEmbedded": - options.youtube.redirectType = "sub_frame" - case "both": - options.youtube.redirectType = "both" - } - for (const service in config.services) { - let oldService - switch (service) { - case "tiktok": - oldService = "tikTok" - break - case "sendFiles": - oldService = "sendTarget" - break - default: - oldService = service - } - options[service].enabled = !r["disable" + utils.camelCase(oldService)] - if (r[oldService + "Frontend"]) { - if (r[oldService + "Frontend"] == "yatte") options[service].frontend = "yattee" - else options[service].frontend = r[oldService + "Frontend"] - } - if (r[oldService + "RedirectType"]) options[service].redirectType = r[oldService + "RedirectType"] - if (r[oldService + "EmbedFrontend"] && (service != "youtube" || r[oldService + "EmbedFrontend"] == "invidious" || r[oldService + "EmbedFrontend"] == "piped")) - options[service].embedFrontend = r[oldService + "EmbedFrontend"] - for (const frontend in config.services[service].frontends) { - if (r[frontend + "Latency"]) latency[frontend] = r[frontend + "Latency"] - for (const network in config.networks) { - let protocol - if (network == "clearnet") protocol = "normal" - else protocol = network - if (r[frontend + utils.camelCase(protocol) + "RedirectsChecks"]) { - options[frontend][network].enabled = r[frontend + utils.camelCase(protocol) + "RedirectsChecks"] - options[frontend][network].custom = r[frontend + utils.camelCase(protocol) + "CustomRedirects"] - for (const instance of options[frontend][network].enabled) { - let i = r.redirects[frontend][network].indexOf(instance) - if (i < 0) options[frontend][network].enabled.splice(i, 1) - } - } - } - } - } - browser.storage.local.set({ options, latency }, () => resolve()) - }) - }) - }) -} - -function processUpdate() { - return new Promise(resolve => { - fetch("/instances/data.json") - .then(response => response.text()) - .then(data => { - fetch("/config/config.json") - .then(response => response.text()) - .then(configData => { - browser.storage.local.get(["options", "blacklists", "targets"], r => { - let redirects = JSON.parse(data) - let options = r.options - let targets = r.targets - let config = JSON.parse(configData) - for (const service in config.services) { - if (!options[service]) options[service] = {} - if (config.services[service].targets == "datajson") { - targets[service] = redirects[service] - delete redirects[service] - } - for (const defaultOption in config.services[service].options) { - if (options[service][defaultOption] === undefined) { - options[service][defaultOption] = config.services[service].options[defaultOption] - } - } - for (const frontend in config.services[service].frontends) { - if (config.services[service].frontends[frontend].instanceList) { - if (!options[frontend]) options[frontend] = {} - for (const network in config.networks) { - if (!options[frontend][network]) { - options[frontend][network] = {} - options[frontend][network].enabled = JSON.parse(data)[frontend][network] - options[frontend][network].custom = [] - if (network == "clearnet") { - for (const blacklist in r.blacklists) { - for (const instance of r.blacklists[blacklist]) { - let i = options[frontend].clearnet.enabled.indexOf(instance) - if (i > -1) options[frontend].clearnet.enabled.splice(i, 1) - } - } - } - } else { - for (const instance of options[frontend][network].enabled) { - let i = redirects[frontend][network].indexOf(instance) - if (i < 0) options[frontend][network].enabled.splice(i, 1) - } - } - } - } - } - } - browser.storage.local.set({ redirects, options, targets }) - resolve() - }) - }) - }) - }) -} - -// For websites that have a strict policy that would not normally allow these frontends to be embedded within the website. -function modifyContentSecurityPolicy(details) { - let isChanged = false - if (details.type == "main_frame") { - for (const header in details.responseHeaders) { - if (details.responseHeaders[header].name == "content-security-policy") { - let instancesList = [] - for (const service in config.services) { - if (config.services[service].embeddable) { - for (const frontend in config.services[service].frontends) { - if (config.services[service].frontends[frontend].embeddable) { - for (const network in config.networks) { - instancesList.push(...options[frontend][network].enabled, ...options[frontend][network].custom) - } - } - } - } - } - let securityPolicyList = details.responseHeaders[header].value.split(";") - for (const i in securityPolicyList) securityPolicyList[i] = securityPolicyList[i].trim() - let newSecurity = "" - for (const item of securityPolicyList) { - if (item.trim() == "") continue - let regex = item.match(/([a-z-]{0,}) (.*)/) - if (regex == null) continue - let [, key, vals] = regex - if (key == "frame-src") vals = vals + " " + instancesList.join(" ") - newSecurity += key + " " + vals + "; " - } - - details.responseHeaders[header].value = newSecurity - isChanged = true - } - } - if (isChanged) return { responseHeaders: details.responseHeaders } - } -} - -export default { - redirect, - computeService, - switchInstance, - reverse, - unifyPreferences, - setRedirects, - initDefaults, - upgradeOptions, - processUpdate, - modifyContentSecurityPolicy, -} +window.browser = window.browser || window.chrome + +import utils from "./utils.js" + +let config, options, redirects, targets + +function init() { + return new Promise(async resolve => { + browser.storage.local.get(["options", "redirects", "targets"], r => { + options = r.options + redirects = r.redirects + targets = r.targets + fetch("/config/config.json") + .then(response => response.text()) + .then(configData => { + config = JSON.parse(configData) + resolve() + }) + }) + }) +} + +init() +browser.storage.onChanged.addListener(init) + +function fetchFrontendInstanceList(service, frontend, redirects, options, config) { + let tmp = [] + if (config.services[service].frontends[frontend].instanceList) { + for (const network in config.networks) { + tmp.push(...redirects[network], ...options[frontend][network].custom) + } + } else if (config.services[service].frontends[frontend].singleInstance) tmp = config.services[service].frontends[frontend].singleInstance + return tmp +} + +function all(service, frontend, options, config, redirects) { + let instances = [] + if (!frontend) { + for (const frontend in config.services[service].frontends) { + instances.push(...fetchFrontendInstanceList(service, frontend, redirects[frontend], options, config)) + } + } else { + instances.push(...fetchFrontendInstanceList(service, frontend, redirects[frontend], options, config)) + } + return instances +} + +function regexArray(service, url, config) { + if (config.services[service].targets == "datajson") { + if (targets[service].includes(utils.protocolHost(url))) return true + } else { + const targetList = config.services[service].targets + for (const targetString in targetList) { + const target = new RegExp(targetList[targetString]) + if (target.test(url.href)) return true + } + } + return false +} + +function redirect(url, type, initiator, forceRedirection) { + if (type != "main_frame" && type != "sub_frame") return + let randomInstance + let frontend + for (const service in config.services) { + if (!forceRedirection && !options[service].enabled) continue + if (config.services[service].embeddable && type != options[service].redirectType && options[service].redirectType != "both") continue + if (!config.services[service].embeddable && type != "main_frame") continue + // let targets = new RegExp(config.services[service].targets.join("|"), "i") + + if (!regexArray(service, url, config)) continue + // if (initiator) { + // console.log(initiator.host) + // if (targets.test(initiator.host)) continue + // //if (all(service, null, options, config, redirects).includes(initiator.origin) && reverse(initiator) == url) return "BYPASSTAB" + // } + + if (Object.keys(config.services[service].frontends).length > 1) { + if (type == "sub_frame" && config.services[service].embeddable && !config.services[service].frontends[options[service].frontend].embeddable) frontend = options[service].embedFrontend + else frontend = options[service].frontend + } else frontend = Object.keys(config.services[service].frontends)[0] + + if (config.services[service].frontends[frontend].instanceList) { + let instanceList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom] + if (instanceList.length === 0 && options.networkFallback) instanceList = [...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom] + if (instanceList.length === 0) return + randomInstance = utils.getRandomInstance(instanceList) + } else if (config.services[service].frontends[frontend].singleInstance) randomInstance = config.services[service].frontends[frontend].singleInstance + break + } + if (!frontend) return + + // Here is a (temperory) space for defining constants required in 2 or more switch cases. + // When possible, try have the two switch cases share all their code as done with searx and searxng. + // Do not do that when they do not share 100% of their code. + + const mapCentreRegex = /@(-?\d[0-9.]*),(-?\d[0-9.]*),(\d{1,2})[.z]/ + const dataLatLngRegex = /!3d(-?[0-9]{1,}.[0-9]{1,})!4d(-?[0-9]{1,}.[0-9]{1,})/ + const placeRegex = /\/place\/(.*)\// + function convertMapCentre() { + let [lat, lon, zoom] = [null, null, null] + if (url.pathname.match(mapCentreRegex)) { + // Set map centre if present + ;[lat, lon, zoom] = url.pathname.match(mapCentreRegex) + } else if (url.searchParams.has("center")) { + ;[lat, lon] = url.searchParams.get("center").split(",") + zoom = url.searchParams.get("zoom") ?? "17" + } + return [zoom, lon, lat] + } + + switch (frontend) { + // This is where all instance-specific code must be ran to convert the service url to one that can be understood by the frontend. + case "beatbump": + return `${randomInstance}${url.pathname}${url.search}` + .replace("/watch?v=", "/listen?id=") + .replace("/channel/", "/artist/") + .replace("/playlist?list=", "/playlist/VL") + .replace(/\/search\?q=.*/, searchQuery => searchQuery.replace("?q=", "/") + "?filter=all") + case "hyperpipe": + return `${randomInstance}${url.pathname}${url.search}`.replace(/\/search\?q=.*/, searchQuery => searchQuery.replace("?q=", "/")) + case "bibliogram": + const reservedPaths = ["u", "p", "privacy"] + if (url.pathname === "/" || reservedPaths.includes(url.pathname.split("/")[1])) return `${randomInstance}${url.pathname}${url.search}` + if (url.pathname.startsWith("/reel") || url.pathname.startsWith("/tv")) return `${randomInstance}/p${url.pathname.replace(/\/reel|\/tv/i, "")}${url.search}` + else return `${randomInstance}/u${url.pathname}${url.search}` // Likely a user profile, redirect to '/u/...' + case "lbryDesktop": + return url.href.replace(/^https?:\/{2}odysee\.com\//, "lbry://").replace(/:(?=[a-zA-Z0-9])/g, "#") + case "neuters": + if (url.pathname.startsWith("/article/") || url.pathname.startsWith("/pf/") || url.pathname.startsWith("/arc/") || url.pathname.startsWith("/resizer/")) return null + else if (url.pathname.endsWith("/")) return `${randomInstance}${url.pathname}` + else return `${randomInstance}${url.pathname}/` + case "searx": + case "searxng": + return `${randomInstance}/?q=${encodeURIComponent(url.searchParams.get("q"))}` + case "whoogle": + return `${randomInstance}/search?q=${encodeURIComponent(url.searchParams.get("q"))}` + case "librex": + return `${randomInstance}/search.php?q=${encodeURIComponent(url.searchParams.get("q"))}` + case "send": + return randomInstance + case "nitter": + let search = new URLSearchParams(url.search) + + search.delete("ref_src") + search.delete("ref_url") + + search = search.toString() + if (search !== "") search = `?${search}` + + if (url.host.split(".")[0] === "pbs" || url.host.split(".")[0] === "video") { + try { + const [, id, format, extra] = search.match(/(.*)\?format=(.*)&(.*)/) + const query = encodeURIComponent(`${id}.${format}?${extra}`) + return `${randomInstance}/pic${url.pathname}${query}` + } catch { + return `${randomInstance}/pic${url.pathname}${search}` + } + } + + if (url.pathname.split("/").includes("tweets")) return `${randomInstance}${url.pathname.replace("/tweets", "")}${search}` + if (url.host == "t.co") return `${randomInstance}/t.co${url.pathname}` + return `${randomInstance}${url.pathname}${search}` + case "yattee": + return url.href.replace(/^https?:\/{2}/, "yattee://") + case "freetube": + return `freetube://https://youtu.be${url.pathname}${url.search}`.replace(/watch\?v=/, "") + case "simplyTranslate": + return `${randomInstance}/${url.search}` + case "libreTranslate": + return `${randomInstance}/${url.search}` + .replace(/(?<=\/?)sl/, "source") + .replace(/(?<=&)tl/, "target") + .replace(/(?<=&)text/, "q") + case "osm": { + if (initiator && initiator.host === "earth.google.com") return + const travelModes = { + driving: "fossgis_osrm_car", + walking: "fossgis_osrm_foot", + bicycling: "fossgis_osrm_bike", + transit: "fossgis_osrm_car", // not implemented on OSM, default to car. + } + + function addressToLatLng(address) { + const xmlhttp = new XMLHttpRequest() + xmlhttp.open("GET", `https://nominatim.openstreetmap.org/search/${address}?format=json&limit=1`, false) + xmlhttp.send() + if (xmlhttp.status === 200) { + const json = JSON.parse(xmlhttp.responseText)[0] + if (json) { + console.log("json", json) + return [`${json.lat},${json.lon}`, `${json.boundingbox[2]},${json.boundingbox[1]},${json.boundingbox[3]},${json.boundingbox[0]}`] + } + } + console.info("Error: Status is " + xmlhttp.status) + } + + let mapCentre = "#" + let prefs = {} + + const mapCentreData = convertMapCentre() + if (mapCentreData[0] && mapCentreData[1] && mapCentreData[2]) mapCentre = `#map=${mapCentreData[0]}/${mapCentreData[1]}/${mapCentreData[2]}` + if (url.searchParams.get("layer")) prefs.layers = osmLayers[url.searchParams.get("layer")] + + if (url.pathname.includes("/embed")) { + // Handle Google Maps Embed API + // https://www.google.com/maps/embed/v1/place?key=AIzaSyD4iE2xVSpkLLOXoyqT-RuPwURN3ddScAI&q=Eiffel+Tower,Paris+France + //console.log("embed life") + + let query = "" + if (url.searchParams.has("q")) query = url.searchParams.get("q") + else if (url.searchParams.has("query")) query = url.searchParams.has("query") + else if (url.searchParams.has("pb")) + try { + query = url.searchParams.get("pb").split(/!2s(.*?)!/)[1] + } catch (error) { + console.error(error) + } // Unable to find map marker in URL. + + let [coords, boundingbox] = addressToLatLng(query) + prefs.bbox = boundingbox + prefs.marker = coords + prefs.layer = "mapnik" + let prefsEncoded = new URLSearchParams(prefs).toString() + return `${randomInstance}/export/embed.html?${prefsEncoded}` + } else if (url.pathname.includes("/dir")) { + // Handle Google Maps Directions + // https://www.google.com/maps/dir/?api=1&origin=Space+Needle+Seattle+WA&destination=Pike+Place+Market+Seattle+WA&travelmode=bicycling + + let travMod = url.searchParams.get("travelmode") + if (url.searchParams.has("travelmode")) prefs.engine = travelModes[travMod] + + let orgVal = url.searchParams.get("origin") + let destVal = url.searchParams.get("destination") + + let org = addressToLatLng(orgVal) + let dest = addressToLatLng(destVal) + prefs.route = `${org};${dest}` + + let prefsEncoded = new URLSearchParams(prefs).toString() + return `${randomInstance}/directions?${prefsEncoded}${mapCentre}` + } else if (url.pathname.includes("data=") && url.pathname.match(dataLatLngRegex)) { + // Get marker from data attribute + // https://www.google.com/maps/place/41%C2%B001'58.2%22N+40%C2%B029'18.2%22E/@41.032833,40.4862063,17z/data=!3m1!4b1!4m6!3m5!1s0x0:0xf64286eaf72fc49d!7e2!8m2!3d41.0328329!4d40.4883948 + //console.log("data life") + + let [, mlat, mlon] = url.pathname.match(dataLatLngRegex) + + return `${randomInstance}/search?query=${mlat}%2C${mlon}` + } else if (url.searchParams.has("ll")) { + // Get marker from ll param + // https://maps.google.com/?ll=38.882147,-76.99017 + //console.log("ll life") + + const [mlat, mlon] = url.searchParams.get("ll").split(",") + + return `${randomInstance}/search?query=${mlat}%2C${mlon}` + } else if (url.searchParams.has("viewpoint")) { + // Get marker from viewpoint param. + // https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=48.857832,2.295226&heading=-45&pitch=38&fov=80 + //console.log("viewpoint life") + + const [mlat, mlon] = url.searchParams.get("viewpoint").split(",") + + return `${randomInstance}/search?query=${mlat}%2C${mlon}` + } else { + // Use query as search if present. + //console.log("normal life") + + let query + if (url.searchParams.has("q")) query = url.searchParams.get("q") + else if (url.searchParams.has("query")) query = url.searchParams.get("query") + else if (url.pathname.match(placeRegex)) query = url.pathname.match(placeRegex)[1] + + let prefsEncoded = new URLSearchParams(prefs).toString() + if (query) return `${randomInstance}/search?query="${query}${mapCentre}&${prefsEncoded}` + } + + let prefsEncoded = new URLSearchParams(prefs).toString() + console.log("mapCentre", mapCentre) + console.log("prefs", prefs) + console.log("prefsEncoded", prefsEncoded) + return `${randomInstance}/${mapCentre}&${prefsEncoded}` + } + case "facil": { + if (initiator && initiator.host === "earth.google.com") return + const travelModes = { + driving: "car", + walking: "pedestrian", + bicycling: "bicycle", + transit: "car", // not implemented on Facil, default to car. + } + const mapCentreData = convertMapCentre() + let mapCentre = "#" + if (mapCentreData[0] && mapCentreData[1] && mapCentreData[2]) mapCentre = `#${mapCentreData[0]}/${mapCentreData[1]}/${mapCentreData[2]}` + + if (url.pathname.includes("/embed")) { + // Handle Google Maps Embed API + // https://www.google.com/maps/embed/v1/place?key=AIzaSyD4iE2xVSpkLLOXoyqT-RuPwURN3ddScAI&q=Eiffel+Tower,Paris+France + //console.log("embed life") + + let query = "" + if (url.searchParams.has("q")) query = url.searchParams.get("q") + else if (url.searchParams.has("query")) query = url.searchParams.has("query") + else if (url.searchParams.has("pb")) + try { + query = url.searchParams.get("pb").split(/!2s(.*?)!/)[1] + } catch (error) { + console.error(error) + } // Unable to find map marker in URL. + + return `${randomInstance}/#q=${query}` + } else if (url.pathname.includes("/dir")) { + // Handle Google Maps Directions + // https://www.google.com/maps/dir/?api=1&origin=Space+Needle+Seattle+WA&destination=Pike+Place+Market+Seattle+WA&travelmode=bicycling + + let travMod = url.searchParams.get("travelmode") + + let orgVal = url.searchParams.get("origin") + let destVal = url.searchParams.get("destination") + + return `${randomInstance}/#q=${orgVal}%20to%20${destVal}%20by%20${travelModes[travMod]}` + } else if (url.pathname.includes("data=") && url.pathname.match(dataLatLngRegex)) { + // Get marker from data attribute + // https://www.google.com/maps/place/41%C2%B001'58.2%22N+40%C2%B029'18.2%22E/@41.032833,40.4862063,17z/data=!3m1!4b1!4m6!3m5!1s0x0:0xf64286eaf72fc49d!7e2!8m2!3d41.0328329!4d40.4883948 + //console.log("data life") + + let [, mlat, mlon] = url.pathname.match(dataLatLngRegex) + + return `${randomInstance}/#q=${mlat}%2C${mlon}` + } else if (url.searchParams.has("ll")) { + // Get marker from ll param + // https://maps.google.com/?ll=38.882147,-76.99017 + //console.log("ll life") + + const [mlat, mlon] = url.searchParams.get("ll").split(",") + + return `${randomInstance}/#q=${mlat}%2C${mlon}` + } else if (url.searchParams.has("viewpoint")) { + // Get marker from viewpoint param. + // https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=48.857832,2.295226&heading=-45&pitch=38&fov=80 + //console.log("viewpoint life") + + const [mlat, mlon] = url.searchParams.get("viewpoint").split(",") + + return `${randomInstance}/#q=${mlat}%2C${mlon}` + } else { + // Use query as search if present. + //console.log("normal life") + + let query + if (url.searchParams.has("q")) query = url.searchParams.get("q") + else if (url.searchParams.has("query")) query = url.searchParams.get("query") + else if (url.pathname.match(placeRegex)) query = url.pathname.match(placeRegex)[1] + + if (query) return `${randomInstance}/${mapCentre}/Mpnk/${query}` + } + } + case "wikiless": + let GETArguments = [] + if (url.search.length > 0) { + let search = url.search.substring(1) //get rid of '?' + let argstrings = search.split("&") + for (let i = 0; i < argstrings.length; i++) { + let args = argstrings[i].split("=") + GETArguments.push([args[0], args[1]]) + } + } + + let link = `${randomInstance}${url.pathname}` + let urlSplit = url.host.split(".") + if (urlSplit[0] != "wikipedia" && urlSplit[0] != "www") { + if (urlSplit[0] == "m") GETArguments.push(["mobileaction", "toggle_view_mobile"]) + else GETArguments.push(["lang", urlSplit[0]]) + if (urlSplit[1] == "m") GETArguments.push(["mobileaction", "toggle_view_mobile"]) + // wikiless doesn't have mobile view support yet + } + for (let i = 0; i < GETArguments.length; i++) link += (i == 0 ? "?" : "&") + GETArguments[i][0] + "=" + GETArguments[i][1] + return link + + case "lingva": + let params_arr = url.search.split("&") + params_arr[0] = params_arr[0].substring(1) + let params = {} + for (let i = 0; i < params_arr.length; i++) { + let pair = params_arr[i].split("=") + params[pair[0]] = pair[1] + } + if (params.sl && params.tl && params.text) { + return `${randomInstance}/${params.sl}/${params.tl}/${params.text}` + } + return randomInstance + case "breezeWiki": + let wiki = url.hostname.match(/^[a-zA-Z0-9-]+(?=\.fandom\.com)/) + if (wiki == "www" || !wiki) wiki = "" + else wiki = "/" + wiki + if (url.href.search(/Special:Search\?query/) > -1) return `${randomInstance}${wiki}${url.pathname}${url.search}`.replace(/Special:Search\?query/, "search?q").replace(/\/wiki/, "") + else return `${randomInstance}${wiki}${url.pathname}${url.search}` + case "rimgo": + if (url.href.search(/^https?:\/{2}(?:[im]\.)?stack\./) > -1) return `${randomInstance}/stack${url.pathname}${url.search}` + else return `${randomInstance}${url.pathname}${url.search}` + case "libreddit": + const subdomain = url.hostname.match(/^(?:(?:external-)?preview|i)(?=\.redd\.it)/) + if (!subdomain) return `${randomInstance}${url.pathname}${url.search}` + switch (subdomain[0]) { + case "preview": + return `${randomInstance}/preview/pre${url.pathname}${url.search}` + case "external-preview": + return `${randomInstance}/preview/external-pre${url.pathname}${url.search}` + case "i": + return `${randomInstance}/img${url.pathname}` + } + case "teddit": + if (/^(?:(?:external-)?preview|i)\.redd\.it/.test(url.hostname)) { + if (url.search == "") return `${randomInstance}${url.pathname}?teddit_proxy=${url.hostname}` + else return `${randomInstance}${url.pathname}${url.search}&teddit_proxy=${url.hostname}` + } + return `${randomInstance}${url.pathname}${url.search}` + default: + return `${randomInstance}${url.pathname}${url.search}` + } +} + +function computeService(url, returnFrontend) { + return new Promise(resolve => { + fetch("/config/config.json") + .then(response => response.text()) + .then(configData => { + const config = JSON.parse(configData) + browser.storage.local.get(["redirects", "options"], r => { + const redirects = r.redirects + const options = r.options + for (const service in config.services) { + if (regexArray(service, url, config)) { + resolve(service) + return + } else { + for (const frontend in config.services[service].frontends) { + if (all(service, frontend, options, config, redirects).includes(utils.protocolHost(url))) { + if (returnFrontend) resolve([service, frontend, utils.protocolHost(url)]) + else resolve(service) + return + } + } + } + } + resolve() + }) + }) + }) +} + +function switchInstance(url) { + return new Promise(async resolve => { + await init() + const protocolHost = utils.protocolHost(url) + for (const service in config.services) { + if (!all(service, null, options, config, redirects).includes(protocolHost)) continue + + let instancesList + if (Object.keys(config.services[service].frontends).length == 1) { + const frontend = Object.keys(config.services[service].frontends)[0] + instancesList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom] + if (instancesList.length === 0 && options.networkFallback) instancesList = [...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom] + } else { + const frontend = options[service].frontend + instancesList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom] + if (instancesList.length === 0 && options.networkFallback) instancesList = [...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom] + } + + let oldInstance + const i = instancesList.indexOf(protocolHost) + if (i > -1) { + oldInstance = instancesList[i] + instancesList.splice(i, 1) + } + if (instancesList.length === 0) { + resolve() + return + } + const randomInstance = utils.getRandomInstance(instancesList) + const oldUrl = `${oldInstance}${url.pathname}${url.search}` + // This is to make instance switching work when the instance depends on the pathname, eg https://darmarit.org/searx + // Doesn't work because of .includes array method, not a top priotiry atm + resolve(oldUrl.replace(oldInstance, randomInstance)) + return + } + resolve() + }) +} + +function reverse(url, urlString) { + return new Promise(async resolve => { + await init() + let protocolHost + if (!urlString) protocolHost = utils.protocolHost(url) + else protocolHost = url.match(/https?:\/{2}(?:[^\s\/]+\.)+[a-zA-Z0-9]+/)[0] + for (const service in config.services) { + if (!all(service, null, options, config, redirects).includes(protocolHost)) continue + + switch (service) { + case "instagram": + case "youtube": + case "imdb": + case "imgur": + case "tiktok": + case "twitter": + case "reddit": + case "imdb": + case "reuters": + case "quora": + case "medium": + if (!urlString) resolve(config.services[service].url + url.pathname + url.search) + else resolve(url.replace(/https?:\/{2}(?:[^\s\/]+\.)+[a-zA-Z0-9]+/, config.services[service].url)) + return + default: + resolve() + return + } + } + resolve() + }) +} + +function unifyPreferences(url, tabId) { + return new Promise(async resolve => { + await init() + const protocolHost = utils.protocolHost(url) + for (const service in config.services) { + for (const frontend in config.services[service].frontends) { + if (all(service, frontend, options, config, redirects).includes(protocolHost)) { + let instancesList = [...options[frontend][options.network].enabled, ...options[frontend][options.network].custom] + if (options.networkFallback && options.network != "clearnet") instancesList.push(...options[frontend].clearnet.enabled, ...options[frontend].clearnet.custom) + + const frontendObject = config.services[service].frontends[frontend] + if ("cookies" in frontendObject.preferences) { + for (const cookie of frontendObject.preferences.cookies) { + await utils.copyCookie(url, instancesList, cookie) + } + } + if ("localstorage" in frontendObject.preferences) { + browser.storage.local.set({ tmp: [frontend, frontendObject.preferences.localstorage] }) + browser.tabs.executeScript(tabId, { + file: "/assets/javascripts/get-localstorage.js", + runAt: "document_start", + }) + for (const instance of instancesList) + browser.tabs.create({ url: instance }, tab => + browser.tabs.executeScript(tab.id, { + file: "/assets/javascripts/set-localstorage.js", + runAt: "document_start", + }) + ) + } + /* + if ("indexeddb" in frontendObject.preferences) { + } + if ("token" in frontendObject.preferences) { + } + */ + resolve(true) + return + } + } + } + }) +} + +function setRedirects(passedRedirects) { + return new Promise(resolve => { + fetch("/config/config.json") + .then(response => response.text()) + .then(configData => { + browser.storage.local.get(/* [ */ "options" /* , "blacklists"] */, async r => { + let redirects = passedRedirects + let options = r.options + const config = JSON.parse(configData) + let targets = {} + for (const service in config.services) { + if (config.services[service].targets == "datajson") { + targets[service] = redirects[service] + delete redirects[service] + } + for (const frontend in config.services[service].frontends) { + if (config.services[service].frontends[frontend].instanceList) { + for (const network in config.networks) { + for (const instance of options[frontend][network].enabled) { + let i = redirects[frontend][network].indexOf(instance) + if (i < 0) options[frontend][network].enabled.splice(i, 1) + } + } + } + } + /* + for (const frontend in config.services[service].frontends) { + if (config.services[service].frontends[frontend].instanceList) { + for (const network in config.networks) { + options[frontend][network].enabled = redirects[frontend][network] + } + for (const blacklist in r.blacklists) { + for (const instance of blacklist) { + let i = options[frontend].clearnet.enabled.indexOf(instance) + if (i > -1) options[frontend].clearnet.enabled.splice(i, 1) + } + } + } + } + */ + // The above will be implemented with https://github.com/libredirect/libredirect/issues/334 + } + for (const frontend in redirects) { + let exists = false + for (const service in config.services) if (config.services[service].frontends[frontend]) exists = true + if (!exists) delete redirects[frontend] + else for (const network in redirects[frontend]) if (!config.networks[network]) delete redirects[frontend][network] + } + browser.storage.local.set({ redirects, targets, options }, () => resolve()) + }) + }) + }) +} + +function initDefaults() { + return new Promise(resolve => { + fetch("/instances/data.json") + .then(response => response.text()) + .then(data => { + fetch("/config/config.json") + .then(response => response.text()) + .then(configData => { + browser.storage.local.get(["options", "blacklists"], r => { + let redirects = JSON.parse(data) + let options = r.options + let targets = {} + let config = JSON.parse(configData) + const localstorage = {} + const latency = {} + for (const service in config.services) { + options[service] = {} + if (config.services[service].targets == "datajson") { + targets[service] = redirects[service] + delete redirects[service] + } + for (const defaultOption in config.services[service].options) options[service][defaultOption] = config.services[service].options[defaultOption] + for (const frontend in config.services[service].frontends) { + if (config.services[service].frontends[frontend].instanceList) { + options[frontend] = {} + for (const network in config.networks) { + options[frontend][network] = {} + options[frontend][network].enabled = JSON.parse(data)[frontend][network] + options[frontend][network].custom = [] + } + for (const blacklist in r.blacklists) { + for (const instance of r.blacklists[blacklist]) { + let i = options[frontend].clearnet.enabled.indexOf(instance) + if (i > -1) options[frontend].clearnet.enabled.splice(i, 1) + } + } + } + } + } + browser.storage.local.set({ redirects, options, targets, latency, localstorage }) + resolve() + }) + }) + }) + }) +} + +function upgradeOptions() { + return new Promise(resolve => { + fetch("/config/config.json") + .then(response => response.text()) + .then(configData => { + browser.storage.local.get(null, r => { + let options = r.options + let latency = {} + const config = JSON.parse(configData) + options.exceptions = r.exceptions + if (r.theme != "DEFAULT") options.theme = r.theme + options.popupServices = r.popupFrontends + let tmp = options.popupServices.indexOf("tikTok") + if (tmp > -1) { + options.popupServices.splice(tmp, 1) + options.popupServices.push("tiktok") + } + tmp = options.popupServices.indexOf("sendTarget") + if (tmp > -1) { + options.popupServices.splice(tmp, 1) + options.popupServices.push("sendFiles") + } + options.autoRedirect = r.autoRedirect + switch (r.onlyEmbeddedVideo) { + case "onlyNotEmbedded": + options.youtube.redirectType = "main_frame" + case "onlyEmbedded": + options.youtube.redirectType = "sub_frame" + case "both": + options.youtube.redirectType = "both" + } + for (const service in config.services) { + let oldService + switch (service) { + case "tiktok": + oldService = "tikTok" + break + case "sendFiles": + oldService = "sendTarget" + break + default: + oldService = service + } + options[service].enabled = !r["disable" + utils.camelCase(oldService)] + if (r[oldService + "Frontend"]) { + if (r[oldService + "Frontend"] == "yatte") options[service].frontend = "yattee" + else options[service].frontend = r[oldService + "Frontend"] + } + if (r[oldService + "RedirectType"]) options[service].redirectType = r[oldService + "RedirectType"] + if (r[oldService + "EmbedFrontend"] && (service != "youtube" || r[oldService + "EmbedFrontend"] == "invidious" || r[oldService + "EmbedFrontend"] == "piped")) + options[service].embedFrontend = r[oldService + "EmbedFrontend"] + for (const frontend in config.services[service].frontends) { + if (r[frontend + "Latency"]) latency[frontend] = r[frontend + "Latency"] + for (const network in config.networks) { + let protocol + if (network == "clearnet") protocol = "normal" + else protocol = network + if (r[frontend + utils.camelCase(protocol) + "RedirectsChecks"]) { + options[frontend][network].enabled = r[frontend + utils.camelCase(protocol) + "RedirectsChecks"] + options[frontend][network].custom = r[frontend + utils.camelCase(protocol) + "CustomRedirects"] + for (const instance of options[frontend][network].enabled) { + let i = r.redirects[frontend][network].indexOf(instance) + if (i < 0) options[frontend][network].enabled.splice(i, 1) + } + } + } + } + } + browser.storage.local.set({ options, latency }, () => resolve()) + }) + }) + }) +} + +function processUpdate() { + return new Promise(resolve => { + fetch("/instances/data.json") + .then(response => response.text()) + .then(data => { + fetch("/config/config.json") + .then(response => response.text()) + .then(configData => { + browser.storage.local.get(["options", "blacklists", "targets"], r => { + let redirects = JSON.parse(data) + let options = r.options + let targets = r.targets + let config = JSON.parse(configData) + for (const service in config.services) { + if (!options[service]) options[service] = {} + if (config.services[service].targets == "datajson") { + targets[service] = redirects[service] + delete redirects[service] + } + for (const defaultOption in config.services[service].options) { + if (options[service][defaultOption] === undefined) { + options[service][defaultOption] = config.services[service].options[defaultOption] + } + } + for (const frontend in config.services[service].frontends) { + if (config.services[service].frontends[frontend].instanceList) { + if (!options[frontend]) options[frontend] = {} + for (const network in config.networks) { + if (!options[frontend][network]) { + options[frontend][network] = {} + options[frontend][network].enabled = JSON.parse(data)[frontend][network] + options[frontend][network].custom = [] + if (network == "clearnet") { + for (const blacklist in r.blacklists) { + for (const instance of r.blacklists[blacklist]) { + let i = options[frontend].clearnet.enabled.indexOf(instance) + if (i > -1) options[frontend].clearnet.enabled.splice(i, 1) + } + } + } + } else { + for (const instance of options[frontend][network].enabled) { + let i = redirects[frontend][network].indexOf(instance) + if (i < 0) options[frontend][network].enabled.splice(i, 1) + } + } + } + } + } + } + browser.storage.local.set({ redirects, options, targets }) + resolve() + }) + }) + }) + }) +} + +// For websites that have a strict policy that would not normally allow these frontends to be embedded within the website. +function modifyContentSecurityPolicy(details) { + let isChanged = false + if (details.type == "main_frame") { + for (const header in details.responseHeaders) { + if (details.responseHeaders[header].name == "content-security-policy") { + let instancesList = [] + for (const service in config.services) { + if (config.services[service].embeddable) { + for (const frontend in config.services[service].frontends) { + if (config.services[service].frontends[frontend].embeddable) { + for (const network in config.networks) { + instancesList.push(...options[frontend][network].enabled, ...options[frontend][network].custom) + } + } + } + } + } + let securityPolicyList = details.responseHeaders[header].value.split(";") + for (const i in securityPolicyList) securityPolicyList[i] = securityPolicyList[i].trim() + let newSecurity = "" + for (const item of securityPolicyList) { + if (item.trim() == "") continue + let regex = item.match(/([a-z-]{0,}) (.*)/) + if (regex == null) continue + let [, key, vals] = regex + if (key == "frame-src") vals = vals + " " + instancesList.join(" ") + newSecurity += key + " " + vals + "; " + } + + details.responseHeaders[header].value = newSecurity + isChanged = true + } + } + if (isChanged) return { responseHeaders: details.responseHeaders } + } +} + +export default { + redirect, + computeService, + switchInstance, + reverse, + unifyPreferences, + setRedirects, + initDefaults, + upgradeOptions, + processUpdate, + modifyContentSecurityPolicy, +} |