This app provides monitoring and information features for the common freifunk user and the technical stuff of a freifunk community.
Code base is taken from a TUM Practical Course project and added here to see if Freifunk Altdorf can use it.
https://www.freifunk-altdorf.de
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
495 lines
15 KiB
495 lines
15 KiB
"use strict"; |
|
/* eslint-disable no-unused-expressions */ |
|
() => `jsdom 7.x onward only works on Node.js 4 or newer: https://github.com/tmpvar/jsdom#install`; |
|
/* eslint-enable no-unused-expressions */ |
|
|
|
const fs = require("fs"); |
|
const path = require("path"); |
|
const { CookieJar } = require("tough-cookie"); |
|
const MIMEType = require("whatwg-mimetype"); |
|
|
|
const { toFileUrl } = require("./jsdom/utils"); |
|
const documentFeatures = require("./jsdom/browser/documentfeatures"); |
|
const { domToHtml } = require("./jsdom/browser/domtohtml"); |
|
const Window = require("./jsdom/browser/Window"); |
|
const resourceLoader = require("./jsdom/browser/resource-loader"); |
|
const VirtualConsole = require("./jsdom/virtual-console"); |
|
const idlUtils = require("./jsdom/living/generated/utils"); |
|
const Blob = require("./jsdom/living/generated/Blob"); |
|
|
|
const whatwgURL = require("whatwg-url"); |
|
|
|
require("./jsdom/living"); // Enable living standard features |
|
|
|
/* eslint-disable no-restricted-modules */ |
|
// TODO: stop using the built-in URL in favor of the spec-compliant whatwg-url package |
|
// This legacy usage is in the process of being purged. |
|
const URL = require("url"); |
|
/* eslint-enable no-restricted-modules */ |
|
|
|
const canReadFilesFromFS = Boolean(fs.readFile); // in a browserify environment, this isn't present |
|
|
|
exports.createVirtualConsole = function (options) { |
|
return new VirtualConsole(options); |
|
}; |
|
|
|
exports.getVirtualConsole = function (window) { |
|
return window._virtualConsole; |
|
}; |
|
|
|
exports.createCookieJar = function () { |
|
return new CookieJar(null, { looseMode: true }); |
|
}; |
|
|
|
exports.nodeLocation = function (node) { |
|
return idlUtils.implForWrapper(node).__location; |
|
}; |
|
|
|
exports.reconfigureWindow = function (window, newProps) { |
|
if ("top" in newProps) { |
|
window._top = newProps.top; |
|
} |
|
}; |
|
|
|
exports.changeURL = function (window, urlString) { |
|
const doc = idlUtils.implForWrapper(window._document); |
|
|
|
const url = whatwgURL.parseURL(urlString); |
|
|
|
if (url === null) { |
|
throw new TypeError(`Could not parse "${urlString}" as a URL`); |
|
} |
|
|
|
doc._URL = url; |
|
doc.origin = whatwgURL.serializeURLOrigin(doc._URL); |
|
}; |
|
|
|
// Proxy to features module |
|
Object.defineProperty(exports, "defaultDocumentFeatures", { |
|
enumerable: true, |
|
configurable: true, |
|
get() { |
|
return documentFeatures.defaultDocumentFeatures; |
|
}, |
|
set(v) { |
|
documentFeatures.defaultDocumentFeatures = v; |
|
} |
|
}); |
|
|
|
exports.jsdom = function (html, options) { |
|
if (options === undefined) { |
|
options = {}; |
|
} |
|
if (options.parsingMode === undefined || options.parsingMode === "auto") { |
|
options.parsingMode = "html"; |
|
} |
|
|
|
if (options.parsingMode !== "html" && options.parsingMode !== "xml") { |
|
throw new RangeError(`Invalid parsingMode option ${JSON.stringify(options.parsingMode)}; must be either "html", ` + |
|
`"xml", "auto", or undefined`); |
|
} |
|
|
|
options.encoding = options.encoding || "UTF-8"; |
|
|
|
setGlobalDefaultConfig(options); |
|
|
|
// Back-compat hack: we have previously suggested nesting these under document, for jsdom.env at least. |
|
// So we need to support that. |
|
if (options.document) { |
|
if (options.document.cookie !== undefined) { |
|
options.cookie = options.document.cookie; |
|
} |
|
if (options.document.referrer !== undefined) { |
|
options.referrer = options.document.referrer; |
|
} |
|
} |
|
|
|
// Adapt old API `features: { ProcessExternalResources: ["script"] }` to the runScripts option. |
|
// This is part of a larger effort to eventually remove the document features infrastructure entirely. It's unclear |
|
// whether we'll kill the old API or document features first, but as long as old API survives, attempts to kill |
|
// document features will need this kind of adapter. |
|
if (!options.features) { |
|
options.features = exports.defaultDocumentFeatures; |
|
} |
|
if (options.features.ProcessExternalResources === undefined) { |
|
options.features.ProcessExternalResources = ["script"]; |
|
} |
|
const ProcessExternalResources = options.features.ProcessExternalResources || []; |
|
if (ProcessExternalResources === "script" || |
|
(ProcessExternalResources.includes && ProcessExternalResources.includes("script"))) { |
|
options.runScripts = "dangerously"; |
|
} |
|
|
|
if (options.pretendToBeVisual !== undefined) { |
|
options.pretendToBeVisual = Boolean(options.pretendToBeVisual); |
|
} else { |
|
options.pretendToBeVisual = false; |
|
} |
|
|
|
// List options explicitly to be clear which are passed through |
|
const window = new Window({ |
|
parsingMode: options.parsingMode, |
|
parseOptions: options.parseOptions, |
|
contentType: options.contentType, |
|
encoding: options.encoding, |
|
url: options.url, |
|
lastModified: options.lastModified, |
|
referrer: options.referrer, |
|
cookieJar: options.cookieJar, |
|
cookie: options.cookie, |
|
resourceLoader: options.resourceLoader, |
|
deferClose: options.deferClose, |
|
concurrentNodeIterators: options.concurrentNodeIterators, |
|
virtualConsole: options.virtualConsole, |
|
pool: options.pool, |
|
agent: options.agent, |
|
agentClass: options.agentClass, |
|
agentOptions: options.agentOptions, |
|
strictSSL: options.strictSSL, |
|
proxy: options.proxy, |
|
userAgent: options.userAgent, |
|
runScripts: options.runScripts, |
|
pretendToBeVisual: options.pretendToBeVisual |
|
}); |
|
|
|
const documentImpl = idlUtils.implForWrapper(window.document); |
|
documentFeatures.applyDocumentFeatures(documentImpl, options.features); |
|
|
|
if (options.created) { |
|
options.created(null, window.document.defaultView); |
|
} |
|
|
|
if (options.parsingMode === "html") { |
|
if (html === undefined || html === "") { |
|
html = "<html><head></head><body></body></html>"; |
|
} |
|
|
|
window.document.write(html); |
|
} else if (options.parsingMode === "xml") { |
|
if (html !== undefined) { |
|
documentImpl._htmlToDom.appendToDocument(html, documentImpl); |
|
} |
|
} |
|
|
|
if (window.document.close && !options.deferClose) { |
|
window.document.close(); |
|
} |
|
|
|
return window.document; |
|
}; |
|
|
|
exports.jQueryify = exports.jsdom.jQueryify = function (window, jqueryUrl, callback) { |
|
if (!window || !window.document) { |
|
return; |
|
} |
|
|
|
const implImpl = idlUtils.implForWrapper(window.document.implementation); |
|
const oldFeatures = implImpl._features; |
|
const oldRunScripts = window._runScripts; |
|
|
|
implImpl._addFeature("FetchExternalResources", ["script"]); |
|
documentFeatures.contextifyWindow(idlUtils.implForWrapper(window.document)._global); |
|
window._runScripts = "dangerously"; |
|
|
|
const scriptEl = window.document.createElement("script"); |
|
scriptEl.className = "jsdom"; |
|
scriptEl.src = jqueryUrl; |
|
scriptEl.onload = scriptEl.onerror = () => { |
|
implImpl._features = oldFeatures; |
|
window._runScripts = oldRunScripts; |
|
// Can't un-contextify the window. Oh well. That's what we get for such magic behavior in old API. |
|
|
|
if (callback) { |
|
callback(window, window.jQuery); |
|
} |
|
}; |
|
|
|
window.document.body.appendChild(scriptEl); |
|
}; |
|
|
|
exports.env = exports.jsdom.env = function () { |
|
const config = getConfigFromEnvArguments(arguments); |
|
let req = null; |
|
|
|
if (config.file && canReadFilesFromFS) { |
|
req = resourceLoader.readFile( |
|
config.file, |
|
{ defaultEncoding: config.defaultEncoding, detectMetaCharset: true }, |
|
(err, text, res) => { |
|
if (err) { |
|
reportInitError(err, config); |
|
return; |
|
} |
|
|
|
const contentType = new MIMEType(res.headers["content-type"]); |
|
config.encoding = contentType.parameters.get("charset"); |
|
setParsingModeFromExtension(config, config.file); |
|
|
|
config.html = text; |
|
processHTML(config); |
|
} |
|
); |
|
} else if (config.html !== undefined) { |
|
processHTML(config); |
|
} else if (config.url) { |
|
req = handleUrl(config); |
|
} else if (config.somethingToAutodetect !== undefined) { |
|
const url = URL.parse(config.somethingToAutodetect); |
|
if (url.protocol && url.hostname) { |
|
config.url = config.somethingToAutodetect; |
|
req = handleUrl(config.somethingToAutodetect); |
|
} else if (canReadFilesFromFS) { |
|
req = resourceLoader.readFile( |
|
config.somethingToAutodetect, |
|
{ defaultEncoding: config.defaultEncoding, detectMetaCharset: true }, |
|
(err, text, res) => { |
|
if (err) { |
|
if (err.code === "ENOENT" || err.code === "ENAMETOOLONG" || err.code === "ERR_INVALID_ARG_TYPE") { |
|
config.html = config.somethingToAutodetect; |
|
processHTML(config); |
|
} else { |
|
reportInitError(err, config); |
|
} |
|
} else { |
|
const contentType = new MIMEType(res.headers["content-type"]); |
|
config.encoding = contentType.parameters.get("charset"); |
|
setParsingModeFromExtension(config, config.somethingToAutodetect); |
|
|
|
config.html = text; |
|
config.url = toFileUrl(config.somethingToAutodetect); |
|
processHTML(config); |
|
} |
|
} |
|
); |
|
} else { |
|
config.html = config.somethingToAutodetect; |
|
processHTML(config); |
|
} |
|
} |
|
|
|
function handleUrl() { |
|
config.cookieJar = config.cookieJar || exports.createCookieJar(); |
|
|
|
const options = { |
|
defaultEncoding: config.defaultEncoding, |
|
detectMetaCharset: true, |
|
headers: config.headers, |
|
pool: config.pool, |
|
strictSSL: config.strictSSL, |
|
proxy: config.proxy, |
|
cookieJar: config.cookieJar, |
|
userAgent: config.userAgent, |
|
agent: config.agent, |
|
agentClass: config.agentClass, |
|
agentOptions: config.agentOptions |
|
}; |
|
|
|
const { fragment } = whatwgURL.parseURL(config.url); |
|
|
|
return resourceLoader.download(config.url, options, (err, responseText, res) => { |
|
if (err) { |
|
reportInitError(err, config); |
|
return; |
|
} |
|
|
|
// The use of `res.request.uri.href` ensures that `window.location.href` |
|
// is updated when `request` follows redirects. |
|
config.html = responseText; |
|
config.url = res.request.uri.href; |
|
if (fragment) { |
|
config.url += `#${fragment}`; |
|
} |
|
|
|
if (res.headers["last-modified"]) { |
|
config.lastModified = new Date(res.headers["last-modified"]); |
|
} |
|
|
|
const contentType = new MIMEType(res.headers["content-type"]); |
|
if (config.parsingMode === "auto") { |
|
if (contentType.isXML()) { |
|
config.parsingMode = "xml"; |
|
} |
|
} |
|
config.contentType = contentType.essence; |
|
config.encoding = contentType.parameters.get("charset"); |
|
|
|
processHTML(config); |
|
}); |
|
} |
|
return req; |
|
}; |
|
|
|
exports.serializeDocument = function (doc) { |
|
return domToHtml([idlUtils.implForWrapper(doc)]); |
|
}; |
|
|
|
exports.blobToBuffer = function (blob) { |
|
return (Blob.is(blob) && idlUtils.implForWrapper(blob)._buffer) || undefined; |
|
}; |
|
|
|
exports.evalVMScript = (window, script) => { |
|
return script.runInContext(idlUtils.implForWrapper(window._document)._global); |
|
}; |
|
|
|
function processHTML(config) { |
|
const window = exports.jsdom(config.html, config).defaultView; |
|
const implImpl = idlUtils.implForWrapper(window.document.implementation); |
|
const features = JSON.parse(JSON.stringify(implImpl._features)); |
|
|
|
let docsLoaded = 0; |
|
const totalDocs = config.scripts.length + config.src.length; |
|
|
|
if (!window || !window.document) { |
|
reportInitError(new Error("JSDOM: a window object could not be created."), config); |
|
return; |
|
} |
|
|
|
function scriptComplete() { |
|
docsLoaded++; |
|
|
|
if (docsLoaded >= totalDocs) { |
|
implImpl._features = features; |
|
|
|
process.nextTick(() => { |
|
if (config.onload) { |
|
config.onload(window); |
|
} |
|
if (config.done) { |
|
config.done(null, window); |
|
} |
|
}); |
|
} |
|
} |
|
|
|
function handleScriptError() { |
|
// nextTick so that an exception within scriptComplete won't cause |
|
// another script onerror (which would be an infinite loop) |
|
process.nextTick(scriptComplete); |
|
} |
|
|
|
if (config.scripts.length > 0 || config.src.length > 0) { |
|
implImpl._addFeature("FetchExternalResources", ["script"]); |
|
|
|
for (const scriptSrc of config.scripts) { |
|
const script = window.document.createElement("script"); |
|
script.className = "jsdom"; |
|
script.onload = scriptComplete; |
|
script.onerror = handleScriptError; |
|
script.src = scriptSrc; |
|
|
|
window.document.body.appendChild(script); |
|
} |
|
|
|
for (const scriptText of config.src) { |
|
const script = window.document.createElement("script"); |
|
script.onload = scriptComplete; |
|
script.onerror = handleScriptError; |
|
script.text = scriptText; |
|
|
|
window.document.documentElement.appendChild(script); |
|
window.document.documentElement.removeChild(script); |
|
} |
|
} else if (window.document.readyState === "complete") { |
|
scriptComplete(); |
|
} else { |
|
window.addEventListener("load", scriptComplete); |
|
} |
|
} |
|
|
|
function setGlobalDefaultConfig(config) { |
|
config.parseOptions = { locationInfo: true }; |
|
|
|
config.pool = config.pool !== undefined ? config.pool : { maxSockets: 6 }; |
|
|
|
config.agentOptions = config.agentOptions !== undefined ? |
|
config.agentOptions : |
|
{ keepAlive: true, keepAliveMsecs: 115 * 1000 }; |
|
|
|
config.strictSSL = config.strictSSL !== undefined ? config.strictSSL : true; |
|
|
|
config.userAgent = config.userAgent || |
|
`Node.js (${process.platform}; U; rv:${process.version}) AppleWebKit/537.36 (KHTML, like Gecko)`; |
|
} |
|
|
|
function getConfigFromEnvArguments(args) { |
|
const config = {}; |
|
if (typeof args[0] === "object") { |
|
Object.assign(config, args[0]); |
|
} else { |
|
for (const arg of args) { |
|
switch (typeof arg) { |
|
case "string": |
|
config.somethingToAutodetect = arg; |
|
break; |
|
case "function": |
|
config.done = arg; |
|
break; |
|
case "object": |
|
if (Array.isArray(arg)) { |
|
config.scripts = arg; |
|
} else { |
|
Object.assign(config, arg); |
|
} |
|
break; |
|
} |
|
} |
|
} |
|
|
|
if (!config.done && !config.created && !config.onload) { |
|
throw new Error("Must pass a \"created\", \"onload\", or \"done\" option, or a callback, to jsdom.env"); |
|
} |
|
|
|
if (config.somethingToAutodetect === undefined && |
|
config.html === undefined && !config.file && !config.url) { |
|
throw new Error("Must pass a \"html\", \"file\", or \"url\" option, or a string, to jsdom.env"); |
|
} |
|
|
|
config.scripts = ensureArray(config.scripts); |
|
config.src = ensureArray(config.src); |
|
config.parsingMode = config.parsingMode || "auto"; |
|
|
|
config.features = config.features || { |
|
FetchExternalResources: false, |
|
SkipExternalResources: false, |
|
ProcessExternalResources: false // needed since we'll process it inside jsdom.jsdom() |
|
}; |
|
|
|
if (!config.url && config.file) { |
|
config.url = toFileUrl(config.file); |
|
} |
|
|
|
config.defaultEncoding = config.defaultEncoding || "windows-1252"; |
|
|
|
setGlobalDefaultConfig(config); |
|
|
|
if (config.scripts.length > 0 || config.src.length > 0) { |
|
config.features.ProcessExternalResources = ["script"]; |
|
} |
|
return config; |
|
} |
|
|
|
function reportInitError(err, config) { |
|
if (config.created) { |
|
config.created(err); |
|
} |
|
if (config.done) { |
|
config.done(err); |
|
} |
|
} |
|
|
|
function ensureArray(value) { |
|
let array = value || []; |
|
if (typeof array === "string") { |
|
array = [array]; |
|
} |
|
return array; |
|
} |
|
|
|
function setParsingModeFromExtension(config, filename) { |
|
if (config.parsingMode === "auto") { |
|
const ext = path.extname(filename); |
|
if (ext === ".xhtml" || ext === ".xml") { |
|
config.parsingMode = "xml"; |
|
} |
|
} |
|
}
|
|
|