Add HTML sanitization when injecting code in pages in the UI

This commit is contained in:
Théophile Diot 2023-09-28 11:30:45 +01:00
parent 4d50026744
commit ece5ce1cdf
No known key found for this signature in database
GPG Key ID: 248FEA4BAE400D06
13 changed files with 2538 additions and 39 deletions

5
.github/codeql.yml vendored
View File

@ -6,7 +6,8 @@ paths:
- src/ui
- src/common
paths-ignore:
- src/ui/static/tsparticles.bundle.min.js
- src/ui/static/js/utils/flatpickr.js
- src/ui/static/js/tsparticles.bundle.min.js
- src/ui/static/js/editor
- src/ui/static/js/utils/flatpickr.js
- src/ui/static/js/utils/purify
- src/common/core/modsecurity/files

View File

@ -13,6 +13,7 @@ SECURITY.md
tsparticles.bundle.min.js
flatpickr.*
src/ui/static/js/editor/*
src/ui/static/js/utils/purify/*
src/ui/templates/*
datepicker-foundation.css
examples/*

View File

@ -72,12 +72,13 @@ class News {
excerpt,
tags,
date,
lastUpdate,
lastUpdate
);
let cleanHTML = DOMPurify.sanitize(cardHTML);
//add to DOM
document
.querySelector("[data-news-container]")
.insertAdjacentHTML("afterbegin", cardHTML);
.insertAdjacentHTML("afterbegin", cleanHTML);
});
}
@ -190,7 +191,7 @@ class darkMode {
};
const send = await fetch(
`${location.href.split("/").slice(0, -1).join("/")}/darkmode`,
data,
data
);
}
}
@ -230,7 +231,7 @@ class FlashMsg {
flashEl.remove();
//update count
this.flashCount.textContent = document.querySelectorAll(
"[data-flash-message]",
"[data-flash-message]"
).length;
}
} catch (err) {}
@ -299,7 +300,7 @@ const setMenu = new Menu();
const setNewsSidebar = new Sidebar(
"[data-sidebar-info]",
"[data-sidebar-info-open]",
"[data-sidebar-info-close]",
"[data-sidebar-info-close]"
);
const setCheckbox = new Checkbox();
@ -310,7 +311,7 @@ const setDisabledPop = new DisabledPop();
const setFlashSidebar = new Sidebar(
"[data-flash-sidebar]",
"[data-flash-sidebar-open]",
"[data-flash-sidebar-close]",
"[data-flash-sidebar-close]"
);
const setNews = new News();
const setDarkM = new darkMode();

View File

@ -36,7 +36,7 @@ class Dropdown {
const btn = e.target.closest("button");
const btnValue = btn.getAttribute("value");
const btnSetting = btn.getAttribute(
`data-${this.prefix}-setting-select-dropdown-btn`,
`data-${this.prefix}-setting-select-dropdown-btn`
);
//stop if same value to avoid new fetching
const isSameVal = this.isSameValue(btnSetting, btnValue);
@ -57,7 +57,7 @@ class Dropdown {
closeAllDrop() {
const drops = document.querySelectorAll(
`[data-${this.prefix}-setting-select-dropdown]`,
`[data-${this.prefix}-setting-select-dropdown]`
);
drops.forEach((drop) => {
drop.classList.add("hidden");
@ -65,8 +65,8 @@ class Dropdown {
document
.querySelector(
`svg[data-${this.prefix}-setting-select="${drop.getAttribute(
`data-${this.prefix}-setting-select-dropdown`,
)}"]`,
`data-${this.prefix}-setting-select-dropdown`
)}"]`
)
.classList.remove("rotate-180");
});
@ -74,7 +74,7 @@ class Dropdown {
isSameValue(btnSetting, value) {
const selectCustom = document.querySelector(
`[data-${this.prefix}-setting-select-text="${btnSetting}"]`,
`[data-${this.prefix}-setting-select-text="${btnSetting}"]`
);
const currVal = selectCustom.textContent;
return currVal === value ? true : false;
@ -82,30 +82,30 @@ class Dropdown {
setSelectNewValue(btnSetting, value) {
const selectCustom = document.querySelector(
`[data-${this.prefix}-setting-select="${btnSetting}"]`,
`[data-${this.prefix}-setting-select="${btnSetting}"]`
);
selectCustom.querySelector(
`[data-${this.prefix}-setting-select-text]`,
`[data-${this.prefix}-setting-select-text]`
).textContent = value;
}
hideDropdown(btnSetting) {
//hide dropdown
const dropdownEl = document.querySelector(
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`,
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`
);
dropdownEl.classList.add("hidden");
dropdownEl.classList.remove("flex");
//svg effect
const dropdownChevron = document.querySelector(
`svg[data-${this.prefix}-setting-select="${btnSetting}"]`,
`svg[data-${this.prefix}-setting-select="${btnSetting}"]`
);
dropdownChevron.classList.remove("rotate-180");
}
changeDropBtnStyle(btnSetting, selectedBtn) {
const dropdownEl = document.querySelector(
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`,
`[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`
);
//reset dropdown btns
const btnEls = dropdownEl.querySelectorAll("button");
@ -116,7 +116,7 @@ class Dropdown {
"bg-primary",
"bg-primary",
"text-gray-300",
"text-gray-300",
"text-gray-300"
);
btn.classList.add("bg-white", "dark:bg-slate-700", "text-gray-700");
});
@ -124,7 +124,7 @@ class Dropdown {
selectedBtn.classList.remove(
"bg-white",
"dark:bg-slate-700",
"text-gray-700",
"text-gray-700"
);
selectedBtn.classList.add("dark:bg-primary", "bg-primary", "text-gray-300");
}
@ -135,10 +135,10 @@ class Dropdown {
.getAttribute(`data-${this.prefix}-setting-select`);
//toggle dropdown
const dropdownEl = document.querySelector(
`[data-${this.prefix}-setting-select-dropdown="${attribut}"]`,
`[data-${this.prefix}-setting-select-dropdown="${attribut}"]`
);
const dropdownChevron = document.querySelector(
`svg[data-${this.prefix}-setting-select="${attribut}"]`,
`svg[data-${this.prefix}-setting-select="${attribut}"]`
);
dropdownEl.classList.toggle("hidden");
dropdownEl.classList.toggle("flex");
@ -300,7 +300,7 @@ class Upload {
this.dropZoneElement.classList.remove(
"border-solid",
"bg-gray-100",
"dark:bg-slate-700/50",
"dark:bg-slate-700/50"
);
this.dropZoneElement.classList.add("border-dashed");
}
@ -309,7 +309,7 @@ class Upload {
this.dropZoneElement.classList.add(
"border-solid",
"bg-gray-100",
"dark:bg-slate-700/50",
"dark:bg-slate-700/50"
);
this.dropZoneElement.classList.remove("border-dashed");
}
@ -345,13 +345,13 @@ class Upload {
if (xhr.status == 201) {
this.uploadedArea.insertAdjacentHTML(
"afterbegin",
this.fileSuccess(name, fileSize),
this.fileSuccess(name, fileSize)
);
this.allowReload();
} else {
this.uploadedArea.insertAdjacentHTML(
"afterbegin",
this.fileFail(name, fileSize),
this.fileFail(name, fileSize)
);
}
}
@ -408,7 +408,8 @@ class Upload {
</div>
</div>
</div>`;
return str;
let cleanHTML = DOMPurify.sanitize(str);
return cleanHTML;
}
fileFail(name, fileSize) {
@ -435,7 +436,8 @@ class Upload {
</div>
</div>
</div>`;
return str;
let cleanHTML = DOMPurify.sanitize(str);
return cleanHTML;
}
}
@ -448,10 +450,10 @@ class Modal {
this.modalExtInp = this.modal.querySelector("input#external");
this.modalTitle = this.modal.querySelector(
`[data-${this.prefix}-modal-title]`,
`[data-${this.prefix}-modal-title]`
);
this.modalTxt = this.modal.querySelector(
`[data-${this.prefix}-modal-text]`,
`[data-${this.prefix}-modal-text]`
);
this.init();
}

View File

@ -52,18 +52,18 @@ class Select {
try {
if (!e.target.closest("button")) {
const selectEls = document.querySelectorAll(
"div[data-setting-select-dropdown]",
"div[data-setting-select-dropdown]"
);
selectEls.forEach((select) => {
select.classList.add("hidden");
select.classList.remove("flex");
});
const btnEls = document.querySelectorAll(
"button[data-setting-select]",
"button[data-setting-select]"
);
btnEls.forEach((btn) => {
const dropdownChevron = btn.querySelector(
`svg[data-setting-select]`,
`svg[data-setting-select]`
);
dropdownChevron.classList.remove("rotate-180");
});
@ -87,7 +87,7 @@ class Select {
.hasAttribute(`data-setting-select-dropdown-btn`)
) {
const btn = e.target.closest(
`button[data-setting-select-dropdown-btn]`,
`button[data-setting-select-dropdown-btn]`
);
const btnValue = btn.getAttribute("value");
@ -116,7 +116,7 @@ class Select {
//close dropdown
const dropdownChevron = selectCustom.querySelector(
`svg[data-setting-select]`,
`svg[data-setting-select]`
);
dropdownChevron.classList.remove("rotate-180");
@ -139,7 +139,7 @@ class Select {
});
//select new one
const newOption = selectEl.querySelector(
`option[value="${selectedValue}"]`,
`option[value="${selectedValue}"]`
);
newOption.selected = true;
newOption.setAttribute("selected", "");
@ -255,7 +255,8 @@ class DisabledPop {
} bg-blue-500 absolute right-2 rounded-lg px-2 py-1 z-20 dark:brightness-90">
<p class="m-0 text-xs text-white dark:text-gray-100">disabled by ${method}</p>
</div>`;
el.insertAdjacentHTML("beforebegin", popupHTML);
let cleanHTML = DOMPurify.sanitize(popupHTML);
el.insertAdjacentHTML("beforebegin", cleanHTML);
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,362 @@
import { freeze } from './utils.js';
export const html = freeze([
'accept',
'action',
'align',
'alt',
'autocapitalize',
'autocomplete',
'autopictureinpicture',
'autoplay',
'background',
'bgcolor',
'border',
'capture',
'cellpadding',
'cellspacing',
'checked',
'cite',
'class',
'clear',
'color',
'cols',
'colspan',
'controls',
'controlslist',
'coords',
'crossorigin',
'datetime',
'decoding',
'default',
'dir',
'disabled',
'disablepictureinpicture',
'disableremoteplayback',
'download',
'draggable',
'enctype',
'enterkeyhint',
'face',
'for',
'headers',
'height',
'hidden',
'high',
'href',
'hreflang',
'id',
'inputmode',
'integrity',
'ismap',
'kind',
'label',
'lang',
'list',
'loading',
'loop',
'low',
'max',
'maxlength',
'media',
'method',
'min',
'minlength',
'multiple',
'muted',
'name',
'nonce',
'noshade',
'novalidate',
'nowrap',
'open',
'optimum',
'pattern',
'placeholder',
'playsinline',
'poster',
'preload',
'pubdate',
'radiogroup',
'readonly',
'rel',
'required',
'rev',
'reversed',
'role',
'rows',
'rowspan',
'spellcheck',
'scope',
'selected',
'shape',
'size',
'sizes',
'span',
'srclang',
'start',
'src',
'srcset',
'step',
'style',
'summary',
'tabindex',
'title',
'translate',
'type',
'usemap',
'valign',
'value',
'width',
'xmlns',
'slot',
]);
export const svg = freeze([
'accent-height',
'accumulate',
'additive',
'alignment-baseline',
'ascent',
'attributename',
'attributetype',
'azimuth',
'basefrequency',
'baseline-shift',
'begin',
'bias',
'by',
'class',
'clip',
'clippathunits',
'clip-path',
'clip-rule',
'color',
'color-interpolation',
'color-interpolation-filters',
'color-profile',
'color-rendering',
'cx',
'cy',
'd',
'dx',
'dy',
'diffuseconstant',
'direction',
'display',
'divisor',
'dur',
'edgemode',
'elevation',
'end',
'fill',
'fill-opacity',
'fill-rule',
'filter',
'filterunits',
'flood-color',
'flood-opacity',
'font-family',
'font-size',
'font-size-adjust',
'font-stretch',
'font-style',
'font-variant',
'font-weight',
'fx',
'fy',
'g1',
'g2',
'glyph-name',
'glyphref',
'gradientunits',
'gradienttransform',
'height',
'href',
'id',
'image-rendering',
'in',
'in2',
'k',
'k1',
'k2',
'k3',
'k4',
'kerning',
'keypoints',
'keysplines',
'keytimes',
'lang',
'lengthadjust',
'letter-spacing',
'kernelmatrix',
'kernelunitlength',
'lighting-color',
'local',
'marker-end',
'marker-mid',
'marker-start',
'markerheight',
'markerunits',
'markerwidth',
'maskcontentunits',
'maskunits',
'max',
'mask',
'media',
'method',
'mode',
'min',
'name',
'numoctaves',
'offset',
'operator',
'opacity',
'order',
'orient',
'orientation',
'origin',
'overflow',
'paint-order',
'path',
'pathlength',
'patterncontentunits',
'patterntransform',
'patternunits',
'points',
'preservealpha',
'preserveaspectratio',
'primitiveunits',
'r',
'rx',
'ry',
'radius',
'refx',
'refy',
'repeatcount',
'repeatdur',
'restart',
'result',
'rotate',
'scale',
'seed',
'shape-rendering',
'specularconstant',
'specularexponent',
'spreadmethod',
'startoffset',
'stddeviation',
'stitchtiles',
'stop-color',
'stop-opacity',
'stroke-dasharray',
'stroke-dashoffset',
'stroke-linecap',
'stroke-linejoin',
'stroke-miterlimit',
'stroke-opacity',
'stroke',
'stroke-width',
'style',
'surfacescale',
'systemlanguage',
'tabindex',
'targetx',
'targety',
'transform',
'transform-origin',
'text-anchor',
'text-decoration',
'text-rendering',
'textlength',
'type',
'u1',
'u2',
'unicode',
'values',
'viewbox',
'visibility',
'version',
'vert-adv-y',
'vert-origin-x',
'vert-origin-y',
'width',
'word-spacing',
'wrap',
'writing-mode',
'xchannelselector',
'ychannelselector',
'x',
'x1',
'x2',
'xmlns',
'y',
'y1',
'y2',
'z',
'zoomandpan',
]);
export const mathMl = freeze([
'accent',
'accentunder',
'align',
'bevelled',
'close',
'columnsalign',
'columnlines',
'columnspan',
'denomalign',
'depth',
'dir',
'display',
'displaystyle',
'encoding',
'fence',
'frame',
'height',
'href',
'id',
'largeop',
'length',
'linethickness',
'lspace',
'lquote',
'mathbackground',
'mathcolor',
'mathsize',
'mathvariant',
'maxsize',
'minsize',
'movablelimits',
'notation',
'numalign',
'open',
'rowalign',
'rowlines',
'rowspacing',
'rowspan',
'rspace',
'rquote',
'scriptlevel',
'scriptminsize',
'scriptsizemultiplier',
'selection',
'separator',
'separators',
'stretchy',
'subscriptshift',
'supscriptshift',
'symmetric',
'voffset',
'width',
'xmlns',
]);
export const xml = freeze([
'xlink:href',
'xml:id',
'xlink:title',
'xml:space',
'xmlns:xlink',
]);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
import { seal } from './utils.js';
// eslint-disable-next-line unicorn/better-regex
export const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
export const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
export const TMPLIT_EXPR = seal(/\${[\w\W]*}/gm);
export const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/); // eslint-disable-line no-useless-escape
export const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
export const IS_ALLOWED_URI = seal(
/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
);
export const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
export const ATTR_WHITESPACE = seal(
/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
);
export const DOCTYPE_NAME = seal(/^html$/i);

View File

@ -0,0 +1,280 @@
import { freeze } from './utils.js';
export const html = freeze([
'a',
'abbr',
'acronym',
'address',
'area',
'article',
'aside',
'audio',
'b',
'bdi',
'bdo',
'big',
'blink',
'blockquote',
'body',
'br',
'button',
'canvas',
'caption',
'center',
'cite',
'code',
'col',
'colgroup',
'content',
'data',
'datalist',
'dd',
'decorator',
'del',
'details',
'dfn',
'dialog',
'dir',
'div',
'dl',
'dt',
'element',
'em',
'fieldset',
'figcaption',
'figure',
'font',
'footer',
'form',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'head',
'header',
'hgroup',
'hr',
'html',
'i',
'img',
'input',
'ins',
'kbd',
'label',
'legend',
'li',
'main',
'map',
'mark',
'marquee',
'menu',
'menuitem',
'meter',
'nav',
'nobr',
'ol',
'optgroup',
'option',
'output',
'p',
'picture',
'pre',
'progress',
'q',
'rp',
'rt',
'ruby',
's',
'samp',
'section',
'select',
'shadow',
'small',
'source',
'spacer',
'span',
'strike',
'strong',
'style',
'sub',
'summary',
'sup',
'table',
'tbody',
'td',
'template',
'textarea',
'tfoot',
'th',
'thead',
'time',
'tr',
'track',
'tt',
'u',
'ul',
'var',
'video',
'wbr',
]);
// SVG
export const svg = freeze([
'svg',
'a',
'altglyph',
'altglyphdef',
'altglyphitem',
'animatecolor',
'animatemotion',
'animatetransform',
'circle',
'clippath',
'defs',
'desc',
'ellipse',
'filter',
'font',
'g',
'glyph',
'glyphref',
'hkern',
'image',
'line',
'lineargradient',
'marker',
'mask',
'metadata',
'mpath',
'path',
'pattern',
'polygon',
'polyline',
'radialgradient',
'rect',
'stop',
'style',
'switch',
'symbol',
'text',
'textpath',
'title',
'tref',
'tspan',
'view',
'vkern',
]);
export const svgFilters = freeze([
'feBlend',
'feColorMatrix',
'feComponentTransfer',
'feComposite',
'feConvolveMatrix',
'feDiffuseLighting',
'feDisplacementMap',
'feDistantLight',
'feDropShadow',
'feFlood',
'feFuncA',
'feFuncB',
'feFuncG',
'feFuncR',
'feGaussianBlur',
'feImage',
'feMerge',
'feMergeNode',
'feMorphology',
'feOffset',
'fePointLight',
'feSpecularLighting',
'feSpotLight',
'feTile',
'feTurbulence',
]);
// List of SVG elements that are disallowed by default.
// We still need to know them so that we can do namespace
// checks properly in case one wants to add them to
// allow-list.
export const svgDisallowed = freeze([
'animate',
'color-profile',
'cursor',
'discard',
'font-face',
'font-face-format',
'font-face-name',
'font-face-src',
'font-face-uri',
'foreignobject',
'hatch',
'hatchpath',
'mesh',
'meshgradient',
'meshpatch',
'meshrow',
'missing-glyph',
'script',
'set',
'solidcolor',
'unknown',
'use',
]);
export const mathMl = freeze([
'math',
'menclose',
'merror',
'mfenced',
'mfrac',
'mglyph',
'mi',
'mlabeledtr',
'mmultiscripts',
'mn',
'mo',
'mover',
'mpadded',
'mphantom',
'mroot',
'mrow',
'ms',
'mspace',
'msqrt',
'mstyle',
'msub',
'msup',
'msubsup',
'mtable',
'mtd',
'mtext',
'mtr',
'munder',
'munderover',
'mprescripts',
]);
// Similarly to SVG, we want to know all MathML elements,
// even those that we disallow by default.
export const mathMlDisallowed = freeze([
'maction',
'maligngroup',
'malignmark',
'mlongdiv',
'mscarries',
'mscarry',
'msgroup',
'mstack',
'msline',
'msrow',
'semantics',
'annotation',
'annotation-xml',
'mprescripts',
'none',
]);
export const text = freeze(['#text']);

View File

@ -0,0 +1,193 @@
const {
entries,
setPrototypeOf,
isFrozen,
getPrototypeOf,
getOwnPropertyDescriptor,
} = Object;
let { freeze, seal, create } = Object; // eslint-disable-line import/no-mutable-exports
let { apply, construct } = typeof Reflect !== 'undefined' && Reflect;
if (!freeze) {
freeze = function (x) {
return x;
};
}
if (!seal) {
seal = function (x) {
return x;
};
}
if (!apply) {
apply = function (fun, thisValue, args) {
return fun.apply(thisValue, args);
};
}
if (!construct) {
construct = function (Func, args) {
return new Func(...args);
};
}
const arrayForEach = unapply(Array.prototype.forEach);
const arrayIndexOf = unapply(Array.prototype.indexOf);
const arrayPop = unapply(Array.prototype.pop);
const arrayPush = unapply(Array.prototype.push);
const arraySlice = unapply(Array.prototype.slice);
const stringToLowerCase = unapply(String.prototype.toLowerCase);
const stringToString = unapply(String.prototype.toString);
const stringMatch = unapply(String.prototype.match);
const stringReplace = unapply(String.prototype.replace);
const stringIndexOf = unapply(String.prototype.indexOf);
const stringTrim = unapply(String.prototype.trim);
const regExpTest = unapply(RegExp.prototype.test);
const typeErrorCreate = unconstruct(TypeError);
/**
* Creates a new function that calls the given function with a specified thisArg and arguments.
*
* @param {Function} func - The function to be wrapped and called.
* @returns {Function} A new function that calls the given function with a specified thisArg and arguments.
*/
function unapply(func) {
return (thisArg, ...args) => apply(func, thisArg, args);
}
/**
* Creates a new function that constructs an instance of the given constructor function with the provided arguments.
*
* @param {Function} func - The constructor function to be wrapped and called.
* @returns {Function} A new function that constructs an instance of the given constructor function with the provided arguments.
*/
function unconstruct(func) {
return (...args) => construct(func, args);
}
/**
* Add properties to a lookup table
*
* @param {Object} set - The set to which elements will be added.
* @param {Array} array - The array containing elements to be added to the set.
* @param {Function} transformCaseFunc - An optional function to transform the case of each element before adding to the set.
* @returns {Object} The modified set with added elements.
*/
function addToSet(set, array, transformCaseFunc = stringToLowerCase) {
if (setPrototypeOf) {
// Make 'in' and truthy checks like Boolean(set.constructor)
// independent of any properties defined on Object.prototype.
// Prevent prototype setters from intercepting set as a this value.
setPrototypeOf(set, null);
}
let l = array.length;
while (l--) {
let element = array[l];
if (typeof element === 'string') {
const lcElement = transformCaseFunc(element);
if (lcElement !== element) {
// Config presets (e.g. tags.js, attrs.js) are immutable.
if (!isFrozen(array)) {
array[l] = lcElement;
}
element = lcElement;
}
}
set[element] = true;
}
return set;
}
/**
* Shallow clone an object
*
* @param {Object} object - The object to be cloned.
* @returns {Object} A new object that copies the original.
*/
export function clone(object) {
const newObject = create(null);
for (const [property, value] of entries(object)) {
if (getOwnPropertyDescriptor(object, property) !== undefined) {
newObject[property] = value;
}
}
return newObject;
}
/**
* This method automatically checks if the prop is function or getter and behaves accordingly.
*
* @param {Object} object - The object to look up the getter function in its prototype chain.
* @param {String} prop - The property name for which to find the getter function.
* @returns {Function} The getter function found in the prototype chain or a fallback function.
*/
function lookupGetter(object, prop) {
while (object !== null) {
const desc = getOwnPropertyDescriptor(object, prop);
if (desc) {
if (desc.get) {
return unapply(desc.get);
}
if (typeof desc.value === 'function') {
return unapply(desc.value);
}
}
object = getPrototypeOf(object);
}
function fallbackValue(element) {
console.warn('fallback value for', element);
return null;
}
return fallbackValue;
}
export {
// Array
arrayForEach,
arrayIndexOf,
arrayPop,
arrayPush,
arraySlice,
// Object
entries,
freeze,
getPrototypeOf,
getOwnPropertyDescriptor,
isFrozen,
setPrototypeOf,
seal,
create,
// RegExp
regExpTest,
// String
stringIndexOf,
stringMatch,
stringReplace,
stringToLowerCase,
stringToString,
stringTrim,
// Errors
typeErrorCreate,
// Other
lookupGetter,
addToSet,
// Reflect
unapply,
unconstruct,
};

View File

@ -18,7 +18,8 @@
<script type="module" src="./js/global.js"></script>
<script src="./js/editor/ace.js"></script>
<script async src="./js/utils/purify/purify.min.js"></script>
<script async src="./js/editor/ace.js"></script>
{% if current_endpoint == "global_config" %}
<script type="module" src="./js/global_config.js"></script>
@ -33,7 +34,7 @@
{% elif current_endpoint == "logs" %}
<link rel="stylesheet" type="text/css" href="./css/flatpickr.css" />
<link rel="stylesheet" type="text/css" href="./css/flatpickr.dark.css" />
<script type="module" src="./js/utils/flatpickr.js"></script>
<script async type="module" src="./js/utils/flatpickr.js"></script>
<script type="module" src="./js/logs.js"></script>
<link
rel="stylesheet"