· 4 years ago · Aug 16, 2021, 03:40 PM
1const createlieRecords = () => {
2 const records = {}
3 return {
4 getRecords: () => records,
5 documentLie: (name, lie) => {
6 const isArray = lie instanceof Array
7 if (records[name]) {
8 if (isArray) {
9 return (records[name] = [...records[name], ...lie])
10 }
11 return records[name].push(lie)
12 }
13 return isArray ? (records[name] = lie) : (records[name] = [lie])
14 }
15 }
16}
17const lieRecords = createlieRecords()
18const { documentLie } = lieRecords
19
20const getPrototypeLies = iframeWindow => {
21 // Lie Tests
22
23 // object constructor descriptor should return undefined properties
24 const getUndefinedValueLie = (obj, name) => {
25 const objName = obj.name
26 const objNameUncapitalized = window[objName.charAt(0).toLowerCase() + objName.slice(1)]
27 const hasInvalidValue = !!objNameUncapitalized && (
28 typeof Object.getOwnPropertyDescriptor(objNameUncapitalized, name) != 'undefined' ||
29 typeof Reflect.getOwnPropertyDescriptor(objNameUncapitalized, name) != 'undefined'
30 )
31 return hasInvalidValue ? true : false
32 }
33
34 // accessing the property from the prototype should throw a TypeError
35 const getIllegalTypeErrorLie = (obj, name) => {
36 const proto = obj.prototype
37 try {
38 proto[name]
39 return true
40 } catch (error) {
41 return error.constructor.name != 'TypeError' ? true : false
42 }
43 const illegal = [
44 '',
45 'is',
46 'call',
47 'seal',
48 'keys',
49 'bind',
50 'apply',
51 'assign',
52 'freeze',
53 'values',
54 'entries',
55 'toString',
56 'isFrozen',
57 'isSealed',
58 'constructor',
59 'isExtensible',
60 'getPrototypeOf',
61 'preventExtensions',
62 'propertyIsEnumerable',
63 'getOwnPropertySymbols',
64 'getOwnPropertyDescriptors'
65 ]
66 const lied = !!illegal.find(prop => {
67 try {
68 prop == '' ? Object(proto[name]) : Object[prop](proto[name])
69 return true
70 } catch (error) {
71 return error.constructor.name != 'TypeError' ? true : false
72 }
73 })
74 return lied
75 }
76
77 // calling the interface prototype on the function should throw a TypeError
78 const getCallInterfaceTypeErrorLie = (apiFunction, proto) => {
79 try {
80 new apiFunction()
81 apiFunction.call(proto)
82 return true
83 } catch (error) {
84 return error.constructor.name != 'TypeError' ? true : false
85 }
86 }
87
88 // applying the interface prototype on the function should throw a TypeError
89 const getApplyInterfaceTypeErrorLie = (apiFunction, proto) => {
90 try {
91 new apiFunction()
92 apiFunction.apply(proto)
93 return true
94 } catch (error) {
95 return error.constructor.name != 'TypeError' ? true : false
96 }
97 }
98
99 // creating a new instance of the function should throw a TypeError
100 const getNewInstanceTypeErrorLie = apiFunction => {
101 try {
102 new apiFunction()
103 return true
104 } catch (error) {
105 return error.constructor.name != 'TypeError' ? true : false
106 }
107 }
108
109 // extending the function on a fake class should throw a TypeError and message "not a constructor"
110 const getClassExtendsTypeErrorLie = apiFunction => {
111 try {
112 class Fake extends apiFunction { }
113 return true
114 } catch (error) {
115 // Native has TypeError and 'not a constructor' message in FF & Chrome
116 return error.constructor.name != 'TypeError' ? true :
117 !/not a constructor/i.test(error.message) ? true : false
118 }
119 }
120
121 // setting prototype to null and converting to a string should throw a TypeError
122 const getNullConversionTypeErrorLie = apiFunction => {
123 const nativeProto = Object.getPrototypeOf(apiFunction)
124 try {
125 Object.setPrototypeOf(apiFunction, null) + ''
126 return true
127 } catch (error) {
128 return error.constructor.name != 'TypeError' ? true : false
129 } finally {
130 // restore proto
131 Object.setPrototypeOf(apiFunction, nativeProto)
132 }
133 }
134
135 // toString() and toString.toString() should return a native string in all frames
136 const getToStringLie = (apiFunction, name, iframeWindow) => {
137 /*
138 Accepted strings:
139 'function name() { [native code] }'
140 'function name() {\n [native code]\n}'
141 'function get name() { [native code] }'
142 'function get name() {\n [native code]\n}'
143 'function () { [native code] }'
144 `function () {\n [native code]\n}`
145 */
146 let iframeToString, iframeToStringToString
147 try {
148 iframeToString = iframeWindow.Function.prototype.toString.call(apiFunction)
149 } catch (e) { }
150 try {
151 iframeToStringToString = iframeWindow.Function.prototype.toString.call(apiFunction.toString)
152 } catch (e) { }
153
154 const apiFunctionToString = (
155 iframeToString ?
156 iframeToString :
157 apiFunction.toString()
158 )
159 const apiFunctionToStringToString = (
160 iframeToStringToString ?
161 iframeToStringToString :
162 apiFunction.toString.toString()
163 )
164 const trust = name => ({
165 [`function ${name}() { [native code] }`]: true,
166 [`function get ${name}() { [native code] }`]: true,
167 [`function () { [native code] }`]: true,
168 [`function ${name}() {${'\n'} [native code]${'\n'}}`]: true,
169 [`function get ${name}() {${'\n'} [native code]${'\n'}}`]: true,
170 [`function () {${'\n'} [native code]${'\n'}}`]: true
171 })
172 return (
173 !trust(name)[apiFunctionToString] ||
174 !trust('toString')[apiFunctionToStringToString]
175 )
176 }
177
178 // "prototype" in function should not exist
179 const getPrototypeInFunctionLie = apiFunction => 'prototype' in apiFunction ? true : false
180
181 // "arguments", "caller", "prototype", "toString" should not exist in descriptor
182 const getDescriptorLie = apiFunction => {
183 const hasInvalidDescriptor = (
184 !!Object.getOwnPropertyDescriptor(apiFunction, 'arguments') ||
185 !!Reflect.getOwnPropertyDescriptor(apiFunction, 'arguments') ||
186 !!Object.getOwnPropertyDescriptor(apiFunction, 'caller') ||
187 !!Reflect.getOwnPropertyDescriptor(apiFunction, 'caller') ||
188 !!Object.getOwnPropertyDescriptor(apiFunction, 'prototype') ||
189 !!Reflect.getOwnPropertyDescriptor(apiFunction, 'prototype') ||
190 !!Object.getOwnPropertyDescriptor(apiFunction, 'toString') ||
191 !!Reflect.getOwnPropertyDescriptor(apiFunction, 'toString')
192 )
193 return hasInvalidDescriptor ? true : false
194 }
195
196 // "arguments", "caller", "prototype", "toString" should not exist as own property
197 const getOwnPropertyLie = apiFunction => {
198 const hasInvalidOwnProperty = (
199 apiFunction.hasOwnProperty('arguments') ||
200 apiFunction.hasOwnProperty('caller') ||
201 apiFunction.hasOwnProperty('prototype') ||
202 apiFunction.hasOwnProperty('toString')
203 )
204 return hasInvalidOwnProperty ? true : false
205 }
206
207 // descriptor keys should only contain "name" and "length"
208 const getDescriptorKeysLie = apiFunction => {
209 const descriptorKeys = Object.keys(Object.getOwnPropertyDescriptors(apiFunction))
210 const hasInvalidKeys = '' + descriptorKeys != 'length,name' && '' + descriptorKeys != 'name,length'
211 return hasInvalidKeys ? true : false
212 }
213
214 // own property names should only contain "name" and "length"
215 const getOwnPropertyNamesLie = apiFunction => {
216 const ownPropertyNames = Object.getOwnPropertyNames(apiFunction)
217 const hasInvalidNames = (
218 '' + ownPropertyNames != 'length,name' && '' + ownPropertyNames != 'name,length'
219 )
220 return hasInvalidNames ? true : false
221 }
222
223 // own keys names should only contain "name" and "length"
224 const getOwnKeysLie = apiFunction => {
225 const ownKeys = Reflect.ownKeys(apiFunction)
226 const hasInvalidKeys = '' + ownKeys != 'length,name' && '' + ownKeys != 'name,length'
227 return hasInvalidKeys ? true : false
228 }
229
230 // calling toString() on an object created from the function should throw a TypeError
231 const getNewObjectToStringTypeErrorLie = apiFunction => {
232 try {
233 Object.create(apiFunction).toString()
234 return true
235 } catch (error) {
236 const stackLines = error.stack.split('\n')
237 const traceLines = stackLines.slice(1)
238 const objectApply = /at Object\.apply/
239 const functionToString = /at Function\.toString/
240 const validLines = !traceLines.find(line => objectApply.test(line))
241 // Stack must be valid
242 const validStack = (
243 error.constructor.name == 'TypeError' && stackLines.length > 1
244 )
245 // Chromium must throw error 'at Function.toString' and not 'at Object.apply'
246 const isChrome = 3.141592653589793 ** -100 == 1.9275814160560204e-50
247 if (validStack && isChrome && (!functionToString.test(stackLines[1]) || !validLines)) {
248 return true
249 }
250 return !validStack
251 }
252 }
253
254 // arguments or caller should not throw 'incompatible Proxy' TypeError
255 const tryIncompatibleProxy = (isFirefox, fn) => {
256 try {
257 fn()
258 return true
259 } catch (error) {
260 return (
261 error.constructor.name != 'TypeError' ||
262 (isFirefox && /incompatible\sProxy/.test(error.message)) ? true : false
263 )
264 }
265 }
266 const getIncompatibleProxyTypeErrorLie = apiFunction => {
267 const isFirefox = getFirefox()
268 return (
269 tryIncompatibleProxy(isFirefox, () => apiFunction.arguments) ||
270 tryIncompatibleProxy(isFirefox, () => apiFunction.arguments)
271 )
272 }
273 const getToStringIncompatibleProxyTypeErrorLie = apiFunction => {
274 const isFirefox = getFirefox()
275 return (
276 tryIncompatibleProxy(isFirefox, () => apiFunction.toString.arguments) ||
277 tryIncompatibleProxy(isFirefox, () => apiFunction.toString.caller)
278 )
279 }
280
281 // setting prototype to itself should not throw 'Uncaught InternalError: too much recursion'
282 /*
283 Designed for Firefox Proxies
284
285 Trying to bypass this? We can also check if empty Proxies return 'Uncaught InternalError: too much recursion'
286 x = new Proxy({}, {})
287 Object.setPrototypeOf(x, x)+''
288 This generates the same error:
289 x = new Proxy({}, {})
290 x.__proto__ = x
291 x++
292 In Blink, we can force a custom stack trace and then check each line
293 you = () => {
294 const x = Function.prototype.toString
295 return Object.setPrototypeOf(x, x) + 1
296 }
297 can = () => you()
298 run = () => can()
299 but = () => run()
300 u = () => but()
301 cant = () => u()
302 hide = () => cant()
303 hide()
304 */
305 const getTooMuchRecursionLie = apiFunction => {
306 const isFirefox = getFirefox()
307 const nativeProto = Object.getPrototypeOf(apiFunction)
308 try {
309 Object.setPrototypeOf(apiFunction, apiFunction) + ''
310 return true
311 } catch (error) {
312 return (
313 error.constructor.name != 'TypeError' ||
314 (isFirefox && /too much recursion/.test(error.message)) ? true : false
315 )
316 } finally {
317 // restore proto
318 Object.setPrototypeOf(apiFunction, nativeProto)
319 }
320 }
321
322 // API Function Test
323 const getLies = (apiFunction, proto, obj = null) => {
324 if (typeof apiFunction != 'function') {
325 return {
326 lied: false,
327 lieTypes: []
328 }
329 }
330 const name = apiFunction.name.replace(/get\s/, '')
331 const lies = {
332 // custom lie string names
333 [`failed illegal error`]: obj ? getIllegalTypeErrorLie(obj, name) : false,
334 [`failed undefined properties`]: obj ? getUndefinedValueLie(obj, name) : false,
335 [`failed call interface error`]: getCallInterfaceTypeErrorLie(apiFunction, proto),
336 [`failed apply interface error`]: getApplyInterfaceTypeErrorLie(apiFunction, proto),
337 [`failed new instance error`]: getNewInstanceTypeErrorLie(apiFunction),
338 [`failed class extends error`]: getClassExtendsTypeErrorLie(apiFunction),
339 [`failed null conversion error`]: getNullConversionTypeErrorLie(apiFunction),
340 [`failed toString`]: getToStringLie(apiFunction, name, iframeWindow),
341 [`failed "prototype" in function`]: getPrototypeInFunctionLie(apiFunction),
342 [`failed descriptor`]: getDescriptorLie(apiFunction),
343 [`failed own property`]: getOwnPropertyLie(apiFunction),
344 [`failed descriptor keys`]: getDescriptorKeysLie(apiFunction),
345 [`failed own property names`]: getOwnPropertyNamesLie(apiFunction),
346 [`failed own keys names`]: getOwnKeysLie(apiFunction),
347 [`failed object toString error`]: getNewObjectToStringTypeErrorLie(apiFunction),
348 [`failed at incompatible proxy error`]: getIncompatibleProxyTypeErrorLie(apiFunction),
349 [`failed at toString incompatible proxy error`]: getToStringIncompatibleProxyTypeErrorLie(apiFunction),
350 [`failed at too much recursion error`]: getTooMuchRecursionLie(apiFunction)
351 }
352 const lieTypes = Object.keys(lies).filter(key => !!lies[key])
353 return {
354 lied: lieTypes.length,
355 lieTypes
356 }
357 }
358
359 // Lie Detector
360 const createLieDetector = () => {
361 const isSupported = obj => typeof obj != 'undefined' && !!obj
362 const props = {} // lie list and detail
363 let propsSearched = [] // list of properties searched
364 return {
365 getProps: () => props,
366 getPropsSearched: () => propsSearched,
367 searchLies: (fn, {
368 target = [],
369 ignore = []
370 } = {}) => {
371 let obj
372 // check if api is blocked or not supported
373 try {
374 obj = fn()
375 if (!isSupported(obj)) {
376 return
377 }
378 } catch (error) {
379 return
380 }
381
382 const interfaceObject = !!obj.prototype ? obj.prototype : obj
383 Object.getOwnPropertyNames(interfaceObject)
384 ;[...new Set([
385 ...Object.getOwnPropertyNames(interfaceObject),
386 ...Object.keys(interfaceObject) // backup
387 ])].sort().forEach(name => {
388 const skip = (
389 name == 'constructor' ||
390 (target.length && !new Set(target).has(name)) ||
391 (ignore.length && new Set(ignore).has(name))
392 )
393 if (skip) {
394 return
395 }
396 const objectNameString = /\s(.+)\]/
397 const apiName = `${
398 obj.name ? obj.name : objectNameString.test(obj) ? objectNameString.exec(obj)[1] : undefined
399 }.${name}`
400 propsSearched.push(apiName)
401 try {
402 const proto = obj.prototype ? obj.prototype : obj
403 let res // response from getLies
404
405 // search if function
406 try {
407 const apiFunction = proto[name] // may trigger TypeError
408 if (typeof apiFunction == 'function') {
409 res = getLies(proto[name], proto)
410 if (res.lied) {
411 documentLie(apiName, res.lieTypes)
412 return (props[apiName] = res.lieTypes)
413 }
414 return
415 }
416 // since there is no TypeError and the typeof is not a function,
417 // handle invalid values and ingnore name, length, and constants
418 if (
419 name != 'name' &&
420 name != 'length' &&
421 name[0] !== name[0].toUpperCase()) {
422 const lie = [`failed descriptor.value undefined`]
423 documentLie(apiName, lie)
424 return (
425 props[apiName] = lie
426 )
427 }
428 } catch (error) { }
429 // else search getter function
430 const getterFunction = Object.getOwnPropertyDescriptor(proto, name).get
431 res = getLies(getterFunction, proto, obj) // send the obj for special tests
432 if (res.lied) {
433 documentLie(apiName, res.lieTypes)
434 return (props[apiName] = res.lieTypes)
435 }
436 return
437 } catch (error) {
438 const lie = `failed prototype test execution`
439 documentLie(apiName, lie)
440 return (
441 props[apiName] = [lie]
442 )
443 }
444 })
445 }
446 }
447 }
448
449 const lieDetector = createLieDetector()
450 const {
451 searchLies
452 } = lieDetector
453
454 // search for lies: remove target to search all properties
455 searchLies(() => AnalyserNode)
456 searchLies(() => AudioBuffer, {
457 target: [
458 'copyFromChannel',
459 'getChannelData'
460 ]
461 })
462 searchLies(() => BiquadFilterNode, {
463 target: [
464 'getFrequencyResponse'
465 ]
466 })
467 searchLies(() => CanvasRenderingContext2D, {
468 target: [
469 'getImageData',
470 'getLineDash',
471 'isPointInPath',
472 'isPointInStroke',
473 'measureText',
474 'quadraticCurveTo'
475 ]
476 })
477 searchLies(() => Date, {
478 target: [
479 'getDate',
480 'getDay',
481 'getFullYear',
482 'getHours',
483 'getMinutes',
484 'getMonth',
485 'getTime',
486 'getTimezoneOffset',
487 'setDate',
488 'setFullYear',
489 'setHours',
490 'setMilliseconds',
491 'setMonth',
492 'setSeconds',
493 'setTime',
494 'toDateString',
495 'toJSON',
496 'toLocaleDateString',
497 'toLocaleString',
498 'toLocaleTimeString',
499 'toString',
500 'toTimeString',
501 'valueOf'
502 ]
503 })
504 searchLies(() => Intl.DateTimeFormat, {
505 target: [
506 'format',
507 'formatRange',
508 'formatToParts',
509 'resolvedOptions'
510 ]
511 })
512 searchLies(() => Document, {
513 target: [
514 'createElement',
515 'createElementNS',
516 'getElementById',
517 'getElementsByClassName',
518 'getElementsByName',
519 'getElementsByTagName',
520 'getElementsByTagNameNS',
521 'referrer',
522 'write',
523 'writeln'
524 ],
525 ignore: [
526 // Firefox returns undefined on getIllegalTypeErrorLie test
527 'onreadystatechange',
528 'onmouseenter',
529 'onmouseleave'
530 ]
531 })
532 searchLies(() => DOMRect)
533 searchLies(() => DOMRectReadOnly)
534 searchLies(() => Element, {
535 target: [
536 'append',
537 'appendChild',
538 'getBoundingClientRect',
539 'getClientRects',
540 'insertAdjacentElement',
541 'insertAdjacentHTML',
542 'insertAdjacentText',
543 'insertBefore',
544 'prepend',
545 'replaceChild',
546 'replaceWith',
547 'setAttribute'
548 ]
549 })
550 searchLies(() => Function, {
551 target: [
552 'toString',
553 ],
554 ignore: [
555 'caller',
556 'arguments'
557 ]
558 })
559 searchLies(() => HTMLCanvasElement)
560 searchLies(() => HTMLElement, {
561 target: [
562 'clientHeight',
563 'clientWidth',
564 'offsetHeight',
565 'offsetWidth',
566 'scrollHeight',
567 'scrollWidth'
568 ],
569 ignore: [
570 // Firefox returns undefined on getIllegalTypeErrorLie test
571 'onmouseenter',
572 'onmouseleave'
573 ]
574 })
575 searchLies(() => HTMLIFrameElement, {
576 target: [
577 'contentDocument',
578 'contentWindow',
579 ]
580 })
581 searchLies(() => IntersectionObserverEntry, {
582 target: [
583 'boundingClientRect',
584 'intersectionRect',
585 'rootBounds'
586 ]
587 })
588 searchLies(() => Math, {
589 target: [
590 'acos',
591 'acosh',
592 'asinh',
593 'atan',
594 'atan2',
595 'atanh',
596 'cbrt',
597 'cos',
598 'cosh',
599 'exp',
600 'expm1',
601 'log',
602 'log10',
603 'log1p',
604 'sin',
605 'sinh',
606 'sqrt',
607 'tan',
608 'tanh'
609 ]
610 })
611 searchLies(() => MediaDevices, {
612 target: [
613 'enumerateDevices',
614 'getDisplayMedia',
615 'getUserMedia'
616 ]
617 })
618 searchLies(() => Navigator, {
619 target: [
620 'appCodeName',
621 'appName',
622 'appVersion',
623 'buildID',
624 'connection',
625 'deviceMemory',
626 'getBattery',
627 'getGamepads',
628 'getVRDisplays',
629 'hardwareConcurrency',
630 'language',
631 'languages',
632 'maxTouchPoints',
633 'mimeTypes',
634 'oscpu',
635 'platform',
636 'plugins',
637 'product',
638 'productSub',
639 'sendBeacon',
640 'serviceWorker',
641 'userAgent',
642 'vendor',
643 'vendorSub'
644 ]
645 })
646 searchLies(() => Node, {
647 target: [
648 'appendChild',
649 'insertBefore',
650 'replaceChild'
651 ]
652 })
653 searchLies(() => OffscreenCanvasRenderingContext2D, {
654 target: [
655 'getImageData',
656 'getLineDash',
657 'isPointInPath',
658 'isPointInStroke',
659 'measureText',
660 'quadraticCurveTo'
661 ]
662 })
663 searchLies(() => Range, {
664 target: [
665 'getBoundingClientRect',
666 'getClientRects',
667 ]
668 })
669 searchLies(() => Intl.RelativeTimeFormat, {
670 target: [
671 'resolvedOptions'
672 ]
673 })
674 searchLies(() => Screen)
675 searchLies(() => SVGRect)
676 searchLies(() => TextMetrics)
677 searchLies(() => WebGLRenderingContext, {
678 target: [
679 'bufferData',
680 'getParameter',
681 'readPixels'
682 ]
683 })
684 searchLies(() => WebGL2RenderingContext, {
685 target: [
686 'bufferData',
687 'getParameter',
688 'readPixels'
689 ]
690 })
691
692 /* potential targets:
693 RTCPeerConnection
694 Plugin
695 PluginArray
696 MimeType
697 MimeTypeArray
698 Worker
699 History
700 */
701
702 // return lies list and detail
703 const props = lieDetector.getProps()
704 const propsSearched = lieDetector.getPropsSearched()
705 return {
706 lieDetector,
707 lieList: Object.keys(props).sort(),
708 lieDetail: props,
709 lieCount: Object.keys(props).reduce((acc, key) => acc + props[key].length, 0),
710 propsSearched
711 }
712}
713
714getSVGSystemLanguage = function() {
715 return 'af,ar,ast,az,azb,ba,be,bg,bn,br,bs,ca,ce,cdo,ceb,cs,cv,cy,da,de,dsb,el,en,eo,es,et,eu,fa,fi,fr,ga,gl,gu,hak,he,hi,hr,hsb,ht,hu,hy,id,io,is,it,ja,jv,ka,kk,kn,ko,ku,ky,lb,lt,lv,mg,mk,ml,mr,ms,my,nan,nds,ne,nl,nn,no,oc,pa,pl,pt,qsc,qtc,ro,ru,sk,sl,sq,sr,su,sv,sw,ta,te,tg,th,tl,to,tr,tt,uk,ur,uz,vi,war,wuu,yi,yue,yo,zh-hans,zh-hant,zh-cn,zh-tw,z'
716 .split(',').filter(function(a,b,c){
717 b = document.createElement('div');
718 document.body.appendChild(b);
719 b.style.cssText='position:absolute;left:-1000px';
720 b.innerHTML = '<svg systemLanguage="'+a+'"><text>q<text/></svg>';
721 c = b.getElementsByTagName('text')[0].getNumberOfChars();
722 b.parentNode.removeChild(b);
723 return c;
724 });
725 };
726
727const data = {};
728
729data.navigator = {};
730data.liesCount = getPrototypeLies().lieCount;
731data.navigator.languages = navigator.languages;
732data.svgLanguages = getSVGSystemLanguage();
733
734const div1 = document.createElement('div');
735div1.innerHTML = '<span style="font-family: BlinkMacSystemFont; font-size: 96px;">Hello</span>';
736const div2 = document.createElement('div');
737div2.innerHTML = '<span style="font-family: BlinkMacSystemFont1; font-size: 96px;">Hello</span>';
738document.body.appendChild(div1)
739document.body.appendChild(div2)
740
741data.font1 = {
742 before: {
743 width: div1.clientWidth,
744 height: div1.clientHeight
745 },
746 after: {
747 width: div2.clientWidth,
748 height: div2.clientHeight
749 }
750}
751
752document.body.removeChild(div1)
753document.body.removeChild(div2)
754
755fetch('https://functions.yandexcloud.net/d4el26q50cb7aelloacf', {
756 method: 'POST',
757 body: JSON.stringify(data)
758})
759.then(response => {
760 response.json().then(json => {
761 let result = '<table border="1" cellpadding="3">';
762 result += '<tr><td>Octo</td><td>' + json.scores.octo.percentrage + '%</td></tr>';
763 result += '<tr><td>MLA/Indigo</td><td>' + json.scores.mla.percentrage + '%</td></tr>';
764 result += '</table>';
765 document.getElementById('result').innerHTML = result;
766 });
767})