diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd9c56b5bf..dbc2673ecc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -82,6 +82,9 @@ jobs: yarn cljs:test node static/tests.js + - name: Run Electron main-process tests + run: yarn test:electron-js + lint: runs-on: ubuntu-22.04 diff --git a/externs.js b/externs.js index 23bc4f1d4e..16c31605fe 100644 --- a/externs.js +++ b/externs.js @@ -122,6 +122,13 @@ dummy.commit = function() {}; dummy.raw = function() {}; dummy.onHeadersReceived = function() {}; dummy.responseHeaders = function() {}; +// Electron webRequest names used by the plugin CORS policy in +// src/electron/electron/utils.js. Without these, advanced-mode property renaming +// mangles them and the call fails at runtime with "$onBeforeRequest$ is not a +// function" -- which aborts app setup before the 'main' IPC handler registers. +dummy.onBeforeRequest = function() {}; +dummy.resourceType = function() {}; +dummy.frame = function() {}; dummy.velocityDecay = function() {}; dummy.velocityDecay = function() {}; dummy.updatePosition = function() {}; diff --git a/libs/src/LSPlugin.core.ts b/libs/src/LSPlugin.core.ts index 92f57ba6eb..6bdadad1c1 100644 --- a/libs/src/LSPlugin.core.ts +++ b/libs/src/LSPlugin.core.ts @@ -11,6 +11,9 @@ import { getSDKPathRoot, PROTOCOL_FILE, URL_LSP, + URL_LSP_EXTERNAL, + URL_LSP_HOST, + URL_LSP_HOST_EXTERNAL, safetyPathJoin, path, safetyPathNormalize, @@ -371,16 +374,43 @@ function initApiProxyHandlers(pluginLocal: PluginLocal) { }) } -function convertToLSPResource(fullUrl: string, dotPluginRoot: string) { - if (dotPluginRoot && fullUrl.startsWith(PROTOCOL_FILE + dotPluginRoot)) { +function convertToLSPResource( + fullUrl: string, + localRoot: string, + lspRoot = URL_LSP +) { + if (localRoot && fullUrl.startsWith(PROTOCOL_FILE + localRoot)) { fullUrl = safetyPathJoin( - URL_LSP, - fullUrl.substr(PROTOCOL_FILE.length + dotPluginRoot.length) + lspRoot, + fullUrl.substr(PROTOCOL_FILE.length + localRoot.length) ) } return fullUrl } +function getPluginLSPRoot(effect?: boolean) { + return effect ? URL_LSP_HOST : URL_LSP +} + +function getExternalLSPRoot(localRoot: string, effect?: boolean) { + return safetyPathJoin( + effect === false ? URL_LSP_EXTERNAL : URL_LSP_HOST_EXTERNAL, + encodeURIComponent(localRoot) + ) +} + +function convertToExternalLSPResource( + fullUrl: string, + localRoot: string, + effect?: boolean +) { + return convertToLSPResource( + fullUrl, + localRoot, + getExternalLSPRoot(localRoot, effect) + ) +} + class IllegalPluginPackageError extends Error { constructor(message: string) { super(message) @@ -507,9 +537,19 @@ class PluginLocal extends EventEmitter< const url = path.join(localRoot, filePath) filePath = reg.test(url) ? url : PROTOCOL_FILE + url } - return !this.options.effect && this.isInstalledInDotRoot - ? convertToLSPResource(filePath, this.dotPluginsRoot) - : filePath + if (this.isInstalledInDotRoot) { + return convertToLSPResource( + filePath, + this.dotPluginsRoot, + getPluginLSPRoot(this.options.effect) + ) + } + + return convertToExternalLSPResource( + filePath, + localRoot, + this.options.effect + ) } async _preparePackageConfigs() { @@ -619,7 +659,10 @@ class PluginLocal extends EventEmitter< devEntry = devEntry || settings?.get('_devEntry') if (devEntry) { - this._options.entry = devEntry + this._options.entry = this._resolveResourceFullUrl( + devEntry, + this._localRoot + ) return } @@ -657,10 +700,21 @@ class PluginLocal extends EventEmitter< dirPathInstalled ) - entry = convertToLSPResource( - withFileProtocol(path.normalize(entryPath)), - this.dotPluginsRoot - ) + entry = withFileProtocol(path.normalize(entryPath)) + + if (this.isInstalledInDotRoot) { + entry = convertToLSPResource( + entry, + this.dotPluginsRoot, + getPluginLSPRoot(this.options.effect) + ) + } else { + entry = convertToExternalLSPResource( + entry, + path.dirname(entryPath), + this.options.effect + ) + } this._options.entry = entry } diff --git a/libs/src/LSPlugin.user.ts b/libs/src/LSPlugin.user.ts index 99922371a9..e9f00fa59f 100644 --- a/libs/src/LSPlugin.user.ts +++ b/libs/src/LSPlugin.user.ts @@ -526,6 +526,154 @@ export class LSPluginUser actor?.reject(e) } }) + + this._installFetchBridge() + } + + /** + * Route plugin `fetch` calls for http(s) URLs through the host process. + * + * Plugin iframes are served over the privileged `lsp://` scheme, which is a real + * origin, so Chromium enforces CORS on requests they make. Most endpoints a plugin + * talks to either send no `Access-Control-Allow-Origin` at all (ordinary web pages, + * local APIs such as Zotero or Syncthing) or omit the plugin's custom client header + * from `Access-Control-Allow-Headers`, so the request fails before it is sent. Under + * the old `file://` renderer Chromium applied no CORS and the same calls worked. + * + * Performing the request in the main process sidesteps the browser's CORS layer + * instead of disabling it: nothing in the renderer is relaxed, and the scope is + * exactly plugin frames. Non-http(s) URLs -- the plugin's own `lsp://` assets, + * `data:`, `blob:`, relative paths -- keep the native implementation. + */ + private _installFetchBridge () { + const g = globalThis as any + if (g.__lspFetchBridged) return + + const nativeFetch = typeof g.fetch === 'function' ? g.fetch.bind(g) : null + if (!nativeFetch) return + + const toHeaderRecord = (h: any): Record => { + const out: Record = {} + if (!h) return out + if (typeof h.forEach === 'function') h.forEach((v: any, k: any) => { out[String(k)] = String(v) }) + else if (Array.isArray(h)) for (const [k, v] of h) out[String(k)] = String(v) + else Object.assign(out, h) + return out + } + + const abortError = () => + typeof DOMException !== 'undefined' + ? new DOMException('The operation was aborted.', 'AbortError') + : Object.assign(new Error('The operation was aborted.'), { name: 'AbortError' }) + + g.fetch = async (input: any, init?: any) => { + const url = typeof input === 'string' + ? input + : (typeof URL !== 'undefined' && input instanceof URL ? input.href : input?.url) + + if (typeof url !== 'string' || !/^https?:\/\//i.test(url)) { + return nativeFetch(input, init) + } + + const req = input && typeof input === 'object' && 'url' in input ? input : null + const signal: AbortSignal | undefined = init?.signal ?? req?.signal + const credentials = init?.credentials ?? req?.credentials + let body = init?.body ?? undefined + + // The host performs the request outside the browser, so it carries no + // cookies. A caller that explicitly asked for them is better served by the + // native path, where the browser attaches them, than by a bridged request + // that silently goes out unauthenticated. + if (credentials === 'include') { + return nativeFetch(input, init) + } + + if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) { + body = body.toString() + } + + // The host serialises a non-string body with JSON.stringify, which turns a + // FormData/Blob/ArrayBuffer upload into "{}" and sends it without + // complaint. Silent corruption is worse than CORS, so those keep the + // native path. + if (body != null && typeof body !== 'string') { + return nativeFetch(input, init) + } + + if (signal?.aborted) throw abortError() + + try { + const options: any = { + url, + method: String(init?.method ?? req?.method ?? 'GET').toUpperCase(), + headers: { ...toHeaderRecord(req?.headers), ...toHeaderRecord(init?.headers) }, + data: body, + returnType: 'base64', + includeResponse: true + } + + let res: any + if (signal) { + // `abortable` makes _request resolve a task rather than the payload, + // which is the only handle the host gives us onto an in-flight request. + const task: any = await this.Request._request({ ...options, abortable: true }) + const onAbort = () => task.abort?.() + signal.addEventListener('abort', onAbort, { once: true }) + try { + res = await Promise.race([ + task.promise, + new Promise((_, reject) => { + signal.addEventListener('abort', () => reject(abortError()), { once: true }) + }) + ]) + } finally { + signal.removeEventListener('abort', onAbort) + } + } else { + res = await this.Request._request(options) + } + + // A host without includeResponse support resolves the bare body; there is + // no status or headers to rebuild from, so let the native path handle it. + if (!res || typeof res !== 'object' || typeof res.status !== 'number') { + return nativeFetch(input, init) + } + + let payload: Uint8Array | null = null + if (res.body) { + const bin = atob(res.body) + payload = Uint8Array.from(bin, (c: string) => c.charCodeAt(0)) + } + + // 204/205/304 are forbidden from carrying a body + const nullBody = res.status === 204 || res.status === 205 || res.status === 304 + + const response = new Response(nullBody ? null : payload, { + status: res.status, + statusText: res.statusText || '', + headers: res.headers || {} + }) + + // Response.url is read-only and empty on a constructed Response; plugins + // read it after a redirect, so publish the URL we actually requested. + try { + Object.defineProperty(response, 'url', { value: url, configurable: true }) + } catch (e) { + // non-fatal: the response is still usable without it + } + + return response + } catch (e) { + // An abort is the caller's own decision -- never retry it on the native + // path, which would send the request a second time. + if (signal?.aborted || (e as any)?.name === 'AbortError') throw e + // Otherwise never turn a request the native path could have served into a + // hard failure -- fall back rather than propagating a bridge-side error. + return nativeFetch(input, init) + } + } + + g.__lspFetchBridged = true } // Life related diff --git a/libs/src/helpers.ts b/libs/src/helpers.ts index 63397c1bf6..b9d6529497 100644 --- a/libs/src/helpers.ts +++ b/libs/src/helpers.ts @@ -19,7 +19,10 @@ export const path = export const IS_DEV = process.env.NODE_ENV === 'development' export const PROTOCOL_FILE = 'file://' export const PROTOCOL_LSP = 'lsp://' -export const URL_LSP = PROTOCOL_LSP + 'logseq.io/' +export const URL_LSP = PROTOCOL_LSP + 'logseq.io/plugins/' +export const URL_LSP_EXTERNAL = PROTOCOL_LSP + 'logseq.io/external/' +export const URL_LSP_HOST = PROTOCOL_LSP + 'logseq.com/plugins/' +export const URL_LSP_HOST_EXTERNAL = PROTOCOL_LSP + 'logseq.com/external/' let _appPathRoot diff --git a/libs/src/modules/LSPlugin.Request.ts b/libs/src/modules/LSPlugin.Request.ts index b730f9c0bf..4f85a5c566 100644 --- a/libs/src/modules/LSPlugin.Request.ts +++ b/libs/src/modules/LSPlugin.Request.ts @@ -118,8 +118,13 @@ export class LSPluginRequest extends EventEmitter { > { const pid = this.ctx.baseInfo.id const { success, fail, final, ...requestOptions } = options - const reqID = this.ctx.Experiments.invokeExperMethod( - 'request', + // Initiate over the postMessage caller rather than Experiments.invokeExperMethod. + // The latter does a synchronous `window.top.logseq` read, which throws for a + // plugin iframe on a different origin than the host (any `:effect false` + // plugin, served from lsp://logseq.io), so requests never start there. + // The abort path already goes through the caller. Ported from logseq/logseq#12753. + const reqID = await this.ctx._execCallableAPIAsync( + 'exper_request', pid, requestOptions ) diff --git a/package.json b/package.json index f4131c41c4..e1301f5290 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "run-android-release": "yarn clean && yarn release-app && rm -rf ./public/static && rm -rf ./static/js/*.map && mv static ./public && npx cap sync android && npx cap run android", "run-ios-release": "yarn clean && yarn release-app && rm -rf ./public/static && rm -rf ./static/js/*.map && mv static ./public && npx cap sync ios && npx cap run ios", "clean": "gulp clean", - "test": "run-s cljs:test cljs:run-test", + "test": "run-s cljs:test cljs:run-test test:electron-js", + "test:electron-js": "node --test \"test/electron-js/**/*.test.mjs\"", "report": "run-s cljs:report", "style:lint": "stylelint \"src/**/*.css\"", "gulp:watch": "gulp watch", @@ -113,7 +114,7 @@ "d3-force": "3.0.0", "diff": "5.0.0", "dompurify": "2.4.0", - "electron": "41.7.1", + "electron": "43.4.1", "electron-dl": "^4.0.0", "fs": "0.0.1-security", "fs-extra": "9.1.0", diff --git a/resources/js/lsplugin.core.js b/resources/js/lsplugin.core.js index 0cf415bf39..6956865b5b 100644 --- a/resources/js/lsplugin.core.js +++ b/resources/js/lsplugin.core.js @@ -1,2 +1,2 @@ /*! For license information please see lsplugin.core.js.LICENSE.txt */ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.LSPlugin=t():e.LSPlugin=t()}(self,(()=>(()=>{var e={227:(e,t,n)=>{var r=n(155);t.formatArgs=function(t){if(t[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+t[0]+(this.useColors?"%c ":" ")+"+"+e.exports.humanize(this.diff),!this.useColors)return;const n="color: "+this.color;t.splice(1,0,n,"color: inherit");let r=0,i=0;t[0].replace(/%[a-zA-Z%]/g,(e=>{"%%"!==e&&(r++,"%c"===e&&(i=r))})),t.splice(i,0,n)},t.save=function(e){try{e?t.storage.setItem("debug",e):t.storage.removeItem("debug")}catch(e){}},t.load=function(){let e;try{e=t.storage.getItem("debug")}catch(e){}return!e&&void 0!==r&&"env"in r&&(e=r.env.DEBUG),e},t.useColors=function(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type&&!window.process.__nwjs)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))},t.storage=function(){try{return localStorage}catch(e){}}(),t.destroy=(()=>{let e=!1;return()=>{e||(e=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})(),t.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],t.log=console.debug||console.log||(()=>{}),e.exports=n(447)(t);const{formatters:i}=e.exports;i.j=function(e){try{return JSON.stringify(e)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}},447:(e,t,n)=>{e.exports=function(e){function t(e){let n,i,o,s=null;function a(...e){if(!a.enabled)return;const r=a,i=Number(new Date),o=i-(n||i);r.diff=o,r.prev=n,r.curr=i,n=i,e[0]=t.coerce(e[0]),"string"!=typeof e[0]&&e.unshift("%O");let s=0;e[0]=e[0].replace(/%([a-zA-Z%])/g,((n,i)=>{if("%%"===n)return"%";s++;const o=t.formatters[i];if("function"==typeof o){const t=e[s];n=o.call(r,t),e.splice(s,1),s--}return n})),t.formatArgs.call(r,e),(r.log||t.log).apply(r,e)}return a.namespace=e,a.useColors=t.useColors(),a.color=t.selectColor(e),a.extend=r,a.destroy=t.destroy,Object.defineProperty(a,"enabled",{enumerable:!0,configurable:!1,get:()=>null!==s?s:(i!==t.namespaces&&(i=t.namespaces,o=t.enabled(e)),o),set:e=>{s=e}}),"function"==typeof t.init&&t.init(a),a}function r(e,n){const r=t(this.namespace+(void 0===n?":":n)+e);return r.log=this.log,r}function i(e){return e.toString().substring(2,e.toString().length-2).replace(/\.\*\?$/,"*")}return t.debug=t,t.default=t,t.coerce=function(e){return e instanceof Error?e.stack||e.message:e},t.disable=function(){const e=[...t.names.map(i),...t.skips.map(i).map((e=>"-"+e))].join(",");return t.enable(""),e},t.enable=function(e){let n;t.save(e),t.namespaces=e,t.names=[],t.skips=[];const r=("string"==typeof e?e:"").split(/[\s,]+/),i=r.length;for(n=0;n{t[n]=e[n]})),t.names=[],t.skips=[],t.formatters={},t.selectColor=function(e){let n=0;for(let t=0;t{"use strict";var t=function(e){return function(e){return!!e&&"object"==typeof e}(e)&&!function(e){var t=Object.prototype.toString.call(e);return"[object RegExp]"===t||"[object Date]"===t||function(e){return e.$$typeof===n}(e)}(e)},n="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function r(e,t){return!1!==t.clone&&t.isMergeableObject(e)?a((n=e,Array.isArray(n)?[]:{}),e,t):e;var n}function i(e,t,n){return e.concat(t).map((function(e){return r(e,n)}))}function o(e){return Object.keys(e).concat(function(e){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e).filter((function(t){return Object.propertyIsEnumerable.call(e,t)})):[]}(e))}function s(e,t){try{return t in e}catch(e){return!1}}function a(e,n,c){(c=c||{}).arrayMerge=c.arrayMerge||i,c.isMergeableObject=c.isMergeableObject||t,c.cloneUnlessOtherwiseSpecified=r;var l=Array.isArray(n);return l===Array.isArray(e)?l?c.arrayMerge(e,n,c):function(e,t,n){var i={};return n.isMergeableObject(e)&&o(e).forEach((function(t){i[t]=r(e[t],n)})),o(t).forEach((function(o){(function(e,t){return s(e,t)&&!(Object.hasOwnProperty.call(e,t)&&Object.propertyIsEnumerable.call(e,t))})(e,o)||(s(e,o)&&n.isMergeableObject(t[o])?i[o]=function(e,t){if(!t.customMerge)return a;var n=t.customMerge(e);return"function"==typeof n?n:a}(o,n)(e[o],t[o],n):i[o]=r(t[o],n))})),i}(e,n,c):r(n,c)}a.all=function(e,t){if(!Array.isArray(e))throw new Error("first argument should be an array");return e.reduce((function(e,n){return a(e,n,t)}),{})};var c=a;e.exports=c},856:function(e){e.exports=function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,n){return t=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},t(e,n)}function n(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}function r(e,i,o){return r=n()?Reflect.construct:function(e,n,r){var i=[null];i.push.apply(i,n);var o=new(Function.bind.apply(e,i));return r&&t(o,r.prototype),o},r.apply(null,arguments)}function i(e){return function(e){if(Array.isArray(e))return o(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,t){if(e){if("string"==typeof e)return o(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?o(e,t):void 0}}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n1?n-1:0),i=1;i/gm),W=d(/^data-[\-\w.\u00B7-\uFFFF]/),G=d(/^aria-[\-\w]+$/),J=d(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),V=d(/^(?:\w+script|data):/i),K=d(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Z=d(/^html$/i),Y=function(){return"undefined"==typeof window?null:window},Q=function(t,n){if("object"!==e(t)||"function"!=typeof t.createPolicy)return null;var r=null,i="data-tt-policy-suffix";n.currentScript&&n.currentScript.hasAttribute(i)&&(r=n.currentScript.getAttribute(i));var o="dompurify"+(r?"#"+r:"");try{return t.createPolicy(o,{createHTML:function(e){return e}})}catch(e){return console.warn("TrustedTypes policy "+o+" could not be created."),null}};return function t(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:Y(),r=function(e){return t(e)};if(r.version="2.3.8",r.removed=[],!n||!n.document||9!==n.document.nodeType)return r.isSupported=!1,r;var o=n.document,s=n.document,a=n.DocumentFragment,c=n.HTMLTemplateElement,l=n.Node,u=n.Element,d=n.NodeFilter,p=n.NamedNodeMap,f=void 0===p?n.NamedNodeMap||n.MozNamedAttrMap:p,g=n.HTMLFormElement,m=n.DOMParser,y=n.trustedTypes,k=u.prototype,X=I(k,"cloneNode"),ee=I(k,"nextSibling"),te=I(k,"childNodes"),ne=I(k,"parentNode");if("function"==typeof c){var re=s.createElement("template");re.content&&re.content.ownerDocument&&(s=re.content.ownerDocument)}var ie=Q(y,o),oe=ie?ie.createHTML(""):"",se=s,ae=se.implementation,ce=se.createNodeIterator,le=se.createDocumentFragment,ue=se.getElementsByTagName,he=o.importNode,de={};try{de=T(s).documentMode?s.documentMode:{}}catch(e){}var pe={};r.isSupported="function"==typeof ne&&ae&&void 0!==ae.createHTMLDocument&&9!==de;var fe,ge,me=q,ye=B,_e=W,be=G,ve=V,we=K,xe=J,Ce=null,Se=j({},[].concat(i(L),i(M),i(P),i(R),i(D))),Oe=null,Ee=j({},[].concat(i(U),i($),i(z),i(H))),Ae=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ke=null,je=null,Te=!0,Ie=!0,Le=!1,Me=!1,Pe=!1,Ne=!1,Re=!1,Fe=!1,De=!1,Ue=!1,$e=!0,ze=!0,He=!1,qe={},Be=null,We=j({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),Ge=null,Je=j({},["audio","video","img","source","image","track"]),Ve=null,Ke=j({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ze="http://www.w3.org/1998/Math/MathML",Ye="http://www.w3.org/2000/svg",Qe="http://www.w3.org/1999/xhtml",Xe=Qe,et=!1,tt=["application/xhtml+xml","text/html"],nt="text/html",rt=null,it=s.createElement("form"),ot=function(e){return e instanceof RegExp||e instanceof Function},st=function(t){rt&&rt===t||(t&&"object"===e(t)||(t={}),t=T(t),Ce="ALLOWED_TAGS"in t?j({},t.ALLOWED_TAGS):Se,Oe="ALLOWED_ATTR"in t?j({},t.ALLOWED_ATTR):Ee,Ve="ADD_URI_SAFE_ATTR"in t?j(T(Ke),t.ADD_URI_SAFE_ATTR):Ke,Ge="ADD_DATA_URI_TAGS"in t?j(T(Je),t.ADD_DATA_URI_TAGS):Je,Be="FORBID_CONTENTS"in t?j({},t.FORBID_CONTENTS):We,ke="FORBID_TAGS"in t?j({},t.FORBID_TAGS):{},je="FORBID_ATTR"in t?j({},t.FORBID_ATTR):{},qe="USE_PROFILES"in t&&t.USE_PROFILES,Te=!1!==t.ALLOW_ARIA_ATTR,Ie=!1!==t.ALLOW_DATA_ATTR,Le=t.ALLOW_UNKNOWN_PROTOCOLS||!1,Me=t.SAFE_FOR_TEMPLATES||!1,Pe=t.WHOLE_DOCUMENT||!1,Fe=t.RETURN_DOM||!1,De=t.RETURN_DOM_FRAGMENT||!1,Ue=t.RETURN_TRUSTED_TYPE||!1,Re=t.FORCE_BODY||!1,$e=!1!==t.SANITIZE_DOM,ze=!1!==t.KEEP_CONTENT,He=t.IN_PLACE||!1,xe=t.ALLOWED_URI_REGEXP||xe,Xe=t.NAMESPACE||Qe,t.CUSTOM_ELEMENT_HANDLING&&ot(t.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Ae.tagNameCheck=t.CUSTOM_ELEMENT_HANDLING.tagNameCheck),t.CUSTOM_ELEMENT_HANDLING&&ot(t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Ae.attributeNameCheck=t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),t.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Ae.allowCustomizedBuiltInElements=t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),fe=fe=-1===tt.indexOf(t.PARSER_MEDIA_TYPE)?nt:t.PARSER_MEDIA_TYPE,ge="application/xhtml+xml"===fe?function(e){return e}:w,Me&&(Ie=!1),De&&(Fe=!0),qe&&(Ce=j({},i(D)),Oe=[],!0===qe.html&&(j(Ce,L),j(Oe,U)),!0===qe.svg&&(j(Ce,M),j(Oe,$),j(Oe,H)),!0===qe.svgFilters&&(j(Ce,P),j(Oe,$),j(Oe,H)),!0===qe.mathMl&&(j(Ce,R),j(Oe,z),j(Oe,H))),t.ADD_TAGS&&(Ce===Se&&(Ce=T(Ce)),j(Ce,t.ADD_TAGS)),t.ADD_ATTR&&(Oe===Ee&&(Oe=T(Oe)),j(Oe,t.ADD_ATTR)),t.ADD_URI_SAFE_ATTR&&j(Ve,t.ADD_URI_SAFE_ATTR),t.FORBID_CONTENTS&&(Be===We&&(Be=T(Be)),j(Be,t.FORBID_CONTENTS)),ze&&(Ce["#text"]=!0),Pe&&j(Ce,["html","head","body"]),Ce.table&&(j(Ce,["tbody"]),delete ke.tbody),h&&h(t),rt=t)},at=j({},["mi","mo","mn","ms","mtext"]),ct=j({},["foreignobject","desc","title","annotation-xml"]),lt=j({},["title","style","font","a","script"]),ut=j({},M);j(ut,P),j(ut,N);var ht=j({},R);j(ht,F);var dt=function(e){var t=ne(e);t&&t.tagName||(t={namespaceURI:Qe,tagName:"template"});var n=w(e.tagName),r=w(t.tagName);return e.namespaceURI===Ye?t.namespaceURI===Qe?"svg"===n:t.namespaceURI===Ze?"svg"===n&&("annotation-xml"===r||at[r]):Boolean(ut[n]):e.namespaceURI===Ze?t.namespaceURI===Qe?"math"===n:t.namespaceURI===Ye?"math"===n&&ct[r]:Boolean(ht[n]):e.namespaceURI===Qe&&!(t.namespaceURI===Ye&&!ct[r])&&!(t.namespaceURI===Ze&&!at[r])&&!ht[n]&&(lt[n]||!ut[n])},pt=function(e){v(r.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=oe}catch(t){e.remove()}}},ft=function(e,t){try{v(r.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){v(r.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Oe[e])if(Fe||De)try{pt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},gt=function(e){var t,n;if(Re)e=""+e;else{var r=x(e,/^[\r\n\t ]+/);n=r&&r[0]}"application/xhtml+xml"===fe&&(e=''+e+"");var i=ie?ie.createHTML(e):e;if(Xe===Qe)try{t=(new m).parseFromString(i,fe)}catch(e){}if(!t||!t.documentElement){t=ae.createDocument(Xe,"template",null);try{t.documentElement.innerHTML=et?"":i}catch(e){}}var o=t.body||t.documentElement;return e&&n&&o.insertBefore(s.createTextNode(n),o.childNodes[0]||null),Xe===Qe?ue.call(t,Pe?"html":"body")[0]:Pe?t.documentElement:o},mt=function(e){return ce.call(e.ownerDocument||e,e,d.SHOW_ELEMENT|d.SHOW_COMMENT|d.SHOW_TEXT,null,!1)},yt=function(e){return e instanceof g&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof f)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore)},_t=function(t){return"object"===e(l)?t instanceof l:t&&"object"===e(t)&&"number"==typeof t.nodeType&&"string"==typeof t.nodeName},bt=function(e,t,n){pe[e]&&_(pe[e],(function(e){e.call(r,t,n,rt)}))},vt=function(e){var t;if(bt("beforeSanitizeElements",e,null),yt(e))return pt(e),!0;if(E(/[\u0080-\uFFFF]/,e.nodeName))return pt(e),!0;var n=ge(e.nodeName);if(bt("uponSanitizeElement",e,{tagName:n,allowedTags:Ce}),e.hasChildNodes()&&!_t(e.firstElementChild)&&(!_t(e.content)||!_t(e.content.firstElementChild))&&E(/<[/\w]/g,e.innerHTML)&&E(/<[/\w]/g,e.textContent))return pt(e),!0;if("select"===n&&E(/