Created
November 27, 2025 21:54
-
-
Save UnbuiltAlmond8/bd5642276b702bf02999a62a4de2f0e7 to your computer and use it in GitHub Desktop.
A reverse engineered version of PHPKobo's Element Tab Obfuscator.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (() => { | |
| // Helper: Normalize selectors into an array of DOM elements | |
| const normalizeSelector = (selector, fallback) => { | |
| let result = []; | |
| if (!selector) { | |
| selector = fallback; | |
| } | |
| if (window.Array.isArray(selector)) { | |
| result = selector; | |
| } else if (typeof selector === 'string') { | |
| const nodes = document.querySelectorAll(selector); | |
| result = [].slice.call(nodes); | |
| } else if (selector instanceof window.HTMLElement) { | |
| result.push(selector); | |
| } else if ((selector instanceof window.HTMLCollection) || (selector.constructor.name == 'NodeList')) { | |
| result = [].slice.call(selector); | |
| } | |
| return result; | |
| }; | |
| // Helper: Filter elements to ensure they exist within the document body | |
| const filterVisibleElements = (elementArray) => { | |
| const arr = [] | |
| elementArray.forEach((el) => { | |
| if (document.body.contains(el)) { | |
| arr.push(el); | |
| } | |
| }); | |
| return arr; | |
| }; | |
| const randomInt = (min, max) => { | |
| return window.Math.floor(window.Math.random() * (max - min) + min); | |
| }; | |
| const randomString = (length) => { | |
| let out = ''; | |
| const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; | |
| const alphaLen = alphabet.length; | |
| for (let i = 0; i < length; i++) { | |
| out += alphabet.charAt(window.Math.floor(window.Math.random() * alphaLen)); | |
| } | |
| return out; | |
| }; | |
| const ensureArray = (obj) => { | |
| return (window.Array.isArray(obj)) ? obj : [obj]; | |
| }; | |
| // Configuration Getters | |
| const getConfigValue = (config, property, fallback) => { | |
| return (property in config) ? config[property] : fallback; | |
| }; | |
| const getConfigIntNullable = (config, property, fallback) => { | |
| const result = parseInt(getConfigValue(config, property, fallback)); | |
| return isNaN(result) ? null : result; | |
| }; | |
| const getConfigInt = (config, property, fallback) => { | |
| const result = parseInt(getConfigValue(config, property, fallback)); | |
| return isNaN(result) ? fallback : result; | |
| }; | |
| const getConfigBool = (config, property, fallback) => { | |
| return getConfigValue(config, property, fallback) ? 1 : 0; | |
| }; | |
| // Handles splitting UTF-16 surrogate pairs (emojis, complex chars) correctly | |
| const splitStringWithSurrogates = (inputString) => { | |
| const result = []; | |
| let buffer = []; // Originally string_from_hostname | |
| for (let i = 0; i < inputString.length; i++) { | |
| const charCode = inputString.charCodeAt(i); | |
| // Check for High Surrogate or Low Surrogate | |
| if (((0xD800 <= charCode) && (charCode <= 0xDBFF)) || ((0xDC00 <= charCode) && (charCode <= 0xDFFF))) { | |
| buffer += inputString[i]; | |
| } else { | |
| if (buffer) { | |
| result.push(buffer); | |
| buffer = ''; | |
| } | |
| result.push(inputString[i]); | |
| } | |
| } | |
| if (buffer) { | |
| result.push(buffer); | |
| } | |
| return result; | |
| }; | |
| const removeScriptTags = () => { | |
| let count = 0 | |
| document.querySelectorAll('script').forEach((script) => { | |
| script.remove(); | |
| count++; | |
| }); | |
| return count; | |
| }; | |
| // Originally _FqBsb | |
| const removeComments = () => { | |
| return ((count) => { | |
| ((root, iterator) => { | |
| if (root) { | |
| // Method 1: NodeIterator | |
| ((node, iter) => { | |
| iter = document.createNodeIterator(root, window.NodeFilter.SHOW_COMMENT, (node) => { | |
| return window.NodeFilter.FILTER_ACCEPT; | |
| }); | |
| while ((node = iter.nextNode())) { | |
| node.remove(); | |
| count++; | |
| } | |
| })(); | |
| // Method 2: Recursive Sibling Traversal (Backup/Thorough check) | |
| (iterator = (node, direction) => { | |
| if (node) { | |
| iterator(!direction ? node.previousSibling : node.nextSibling, direction); | |
| if (node.nodeType == window.Node.COMMENT_NODE) { | |
| node.remove(); | |
| count++; | |
| } | |
| } | |
| return iterator; | |
| })(root.previousSibling, 0)(root.nextSibling, 1); | |
| } | |
| })(document.documentElement); | |
| return count; | |
| })(0); | |
| }; | |
| let stats = { | |
| n_run: 0, | |
| n_root: 0, | |
| n_text: 0, | |
| n_char: 0, | |
| n_script: 0, | |
| n_comment: 0 | |
| }; | |
| let errorMessage = 'OK'; | |
| // Global Configuration Variables | |
| let currentConfig, targetSelector, startDelay, obfuscatedTagName, applyClassName, excludeClassName; | |
| let excludedTags, idPrefix, idPrefixLength, isContentEditable, shouldRemoveScripts, shouldRemoveComments; | |
| let stylesheet, usedIds, processedNodesMap, whitespaceRegex; | |
| // Originally eto_config | |
| const parseConfig = (config) => { | |
| try { | |
| if (config === undefined) { | |
| return; | |
| } | |
| currentConfig = Object.assign({}, config); | |
| targetSelector = getConfigValue(currentConfig, 'selector', null); | |
| startDelay = getConfigIntNullable(currentConfig, 'start', 0); | |
| obfuscatedTagName = getConfigValue(currentConfig, 'tag_name', 'eto'); | |
| applyClassName = getConfigValue(currentConfig, 'apply_cname', 'eto-apply'); | |
| excludeClassName = getConfigValue(currentConfig, 'exclude_cname', 'eto-exclude'); | |
| // Originally _GdYbz3 | |
| excludedTags = ensureArray(('exclude_tag' in currentConfig) ? currentConfig.exclude_tag : ['script', 'style', 'textarea', 'select']).map((tag) => tag.toLowerCase()); | |
| idPrefix = getConfigValue(currentConfig, 'id_prefix', 'eto-'); | |
| idPrefixLength = window.Math.max(0, getConfigInt(currentConfig, 'id_prefix_len', 10)); | |
| isContentEditable = getConfigBool(currentConfig, 'contenteditable', 1); | |
| shouldRemoveScripts = getConfigBool(currentConfig, 'remove_script', 1); | |
| shouldRemoveComments = getConfigBool(currentConfig, 'remove_comment', 1); | |
| } catch (error) { | |
| errorMessage = error.name + ': ' + error.message; | |
| } | |
| }; | |
| // Originally _ZaA49f | |
| const initEnvironment = () => { | |
| if (document.currentScript) { | |
| document.currentScript.remove(); | |
| } | |
| const styleEl = document.createElement('style'); | |
| document.head.appendChild(styleEl); | |
| stylesheet = styleEl.sheet; | |
| usedIds = {}; | |
| processedNodesMap = new window.WeakMap(); // Originally weak_map | |
| whitespaceRegex = new window.RegExp("^\\s*$"); // Originally _CRi31 | |
| }; | |
| const generateUniqueId = () => { | |
| let i = 0; | |
| let id; | |
| do { | |
| id = idPrefix + randomString(idPrefixLength + i); | |
| i++; | |
| } while (id in usedIds); | |
| usedIds[id] = 1; | |
| return id; | |
| }; | |
| // Originally _Y77: Injects CSS to display a character via pseudo-element content | |
| const injectCssRule = (elementId, pseudoType, charContent) => { | |
| const selector = '#' + elementId; | |
| // Obfuscate variable name | |
| const randomVarSuffix = randomInt(0, 5) * 2 + ((pseudoType[0] == 'b') ? 0 : 1); // logic creates variance based on 'before' vs 'after' | |
| const varName = '--' + elementId + '-' + randomVarSuffix.toString(); | |
| // Convert char to hex if needed for CSS content | |
| const cssContentValue = (charContent.length == 1) ? "\\" + charContent.codePointAt(0).toString(16) : charContent; | |
| const varDeclaration = varName + ':"' + cssContentValue + '";'; | |
| const rule1 = selector + '{' + varDeclaration + '}'; | |
| stylesheet.insertRule(rule1, 0); | |
| const contentRule = 'content:var(' + varName + ');'; | |
| const rule2 = selector + '::' + pseudoType + '{' + contentRule + '}'; | |
| stylesheet.insertRule(rule2, 0); | |
| }; | |
| // Originally _FkDBQ: Decides if chars go into ::before or ::after | |
| const renderCharsViaCss = (id, charBefore, charAfter) => { | |
| if (charBefore) { | |
| injectCssRule(id, 'before', charBefore); | |
| stats.n_char += charBefore.length; | |
| } | |
| if (charAfter) { | |
| injectCssRule(id, 'after', charAfter); | |
| stats.n_char += charAfter.length; | |
| } | |
| }; | |
| // Originally _H7a6x9: Creates a single wrapper element | |
| const createObfuscatedElement = (charBefore, charAfter) => { | |
| return ((id, element) => { | |
| id = generateUniqueId(); | |
| renderCharsViaCss(id, charBefore, charAfter); | |
| element = document.createElement(obfuscatedTagName); | |
| element.id = id; | |
| return element; | |
| })(); | |
| }; | |
| // Originally _UBx: Recursively builds a tree of elements to hold the string | |
| const buildObfuscatedTree = (charArray) => { | |
| return ((char1, char2, len, wrapper, nextLen, batchSize, batch) => { | |
| len = charArray.length; | |
| // Pop chars for the current node's pseudo-elements | |
| if (len) { | |
| char1 = charArray.shift(); | |
| len--; | |
| if (len) { | |
| char2 = charArray.pop(); | |
| len--; | |
| } | |
| } | |
| wrapper = createObfuscatedElement(char1, char2); | |
| nextLen = charArray.length + 1; | |
| // If chars remain, recurse and append children | |
| while (charArray.length > 0) { | |
| // Randomly determine how many chars the child node handles | |
| batchSize = window.Math.min(charArray.length, randomInt(1, nextLen)); | |
| batch = charArray.slice(0, batchSize); | |
| charArray = charArray.slice(batchSize); | |
| wrapper.appendChild(buildObfuscatedTree(batch)); | |
| } | |
| return wrapper; | |
| })(null, null); | |
| }; | |
| // Originally _C1QEmd: Replaces a Text Node with the obfuscated DOM tree | |
| const obfuscateTextNode = (textNode) => { | |
| return ((textValue) => { | |
| textValue = textNode.nodeValue; | |
| // Only process if it contains actual text (not just whitespace) | |
| if (textValue && !whitespaceRegex.test(textValue)) { | |
| textNode.replaceWith(buildObfuscatedTree(splitStringWithSurrogates(textValue))); | |
| stats.n_text++; | |
| } | |
| })(); | |
| }; | |
| // Originally _Fr5G4v: Checks if a node should be processed | |
| const shouldProcessNode = (node) => { | |
| return ((attrVal) => { | |
| if (processedNodesMap.has(node)) { | |
| return false; | |
| } | |
| if ((excludeClassName != '') && node.classList.contains(excludeClassName)) { | |
| return false; | |
| } | |
| if (excludedTags.indexOf(node.tagName.toLowerCase()) != -1) { | |
| return false; | |
| } | |
| if (isContentEditable) { | |
| attrVal = node.getAttribute('contenteditable'); | |
| if (attrVal && (attrVal.toLowerCase()) == 'true') { | |
| return false; | |
| } | |
| } | |
| return true; | |
| })(); | |
| }; | |
| // Originally _S4a: Recursive DOM walker to find text nodes | |
| const traverseAndCollect = (container, node) => { | |
| return ((i, child, type) => { | |
| for (i = 0; i < node.childNodes.length; i++) { | |
| child = node.childNodes[i]; | |
| type = child.nodeType; | |
| if (type == window.Node.ELEMENT_NODE) { | |
| if (shouldProcessNode(child)) { | |
| traverseAndCollect(container, child) | |
| } | |
| } else if (type == Node.TEXT_NODE) { | |
| container.textNodes.push(child); | |
| } | |
| } | |
| })(); | |
| }; | |
| // Originally _QvkL2: Wrapper to get all text nodes in an element | |
| const getTextNodesInElement = (element) => { | |
| return ((container) => { | |
| container = { | |
| textNodes: [] // Originally _D4ge | |
| }; | |
| traverseAndCollect(container, element); | |
| return container.textNodes; | |
| })(); | |
| }; | |
| // Originally _BFp: Process array of root elements | |
| const processElements = (elements) => { | |
| return ((i, el, textNodes, j, textNode) => { | |
| for (i = 0; i < elements.length; i++) { | |
| el = elements[i]; | |
| if (shouldProcessNode(el)) { | |
| stats.n_root++; | |
| textNodes = getTextNodesInElement(el); | |
| for (j = 0; j < textNodes.length; j++) { | |
| textNode = textNodes[j]; | |
| obfuscateTextNode(textNode); | |
| } | |
| processedNodesMap.set(el, 1); | |
| } | |
| } | |
| })(); | |
| }; | |
| // Originally _Qe63: Main Execution Controller | |
| const executeObfuscation = () => { | |
| try { | |
| return ((targets) => { | |
| stats.n_root = 0; | |
| stats.n_text = 0; | |
| stats.n_char = 0; | |
| stats.n_script = 0; | |
| stats.n_comment = 0; | |
| if (stats.n_run == 0) { | |
| initEnvironment(); | |
| } | |
| stats.n_run++; | |
| if (shouldRemoveScripts) { | |
| stats.n_script = removeScriptTags(); | |
| } | |
| if (shouldRemoveComments) { | |
| stats.n_comment = removeComments(); | |
| } | |
| // Determine targets: specific class or entire body fallback | |
| targets = (applyClassName == '') ? [] : filterVisibleElements(normalizeSelector(targetSelector, '.' + applyClassName)); | |
| if (targets.length == 0) { | |
| targets.push(document.body); | |
| } | |
| processElements(targets); | |
| return stats; | |
| })(); | |
| } catch (error) { | |
| errorMessage = error.name + ': ' + error.message; | |
| return false; | |
| } | |
| }; | |
| const run = () => { | |
| executeObfuscation(); | |
| }; | |
| // Public API mappings | |
| load_eto_config = (config) => parseConfig(config); | |
| runObfuscator = () => run(); | |
| getParams = () => currentConfig; | |
| getStats = () => stats; | |
| getStatus = () => errorMessage; | |
| // Initialization Logic | |
| const defaultConfig = { | |
| apply_cname: '', | |
| exclude_cname: '' | |
| }; | |
| load_eto_config(defaultConfig); | |
| document.addEventListener('DOMContentLoaded', () => { | |
| if (window.Number.isInteger(startDelay)) { | |
| startDelay = window.Math.max(0, startDelay); | |
| if (startDelay == 0) { | |
| runObfuscator(); | |
| } else { | |
| window.setTimeout(() => { | |
| runObfuscator(); // Note: Original called _run() which seemed to be a typo for run() or _KVR() | |
| }, startDelay); | |
| } | |
| } | |
| }); | |
| // --- DRM / Trial Version Check Logic --- | |
| // Checks hostname, URL parameters, and a hardcoded expiry date (Sept 2025) | |
| // If trial, injects a large "Free Trial Version" watermark link. | |
| const isFreeTrial = (() => { | |
| if (document.location.href.indexOf('free-trial-version=etobf') !== -1) { | |
| return true; | |
| } else { | |
| const hostClean = window.location.hostname.replace(/[\d\.\:]/g, ''); | |
| if ((hostClean == '') || hostClean.includes('php') || hostClean.includes('local')) { | |
| return false; | |
| } | |
| // Simple checksum on hostname to limit use | |
| const hostCheck = ((window.Array.from(hostClean).reduce((sum, char) => sum + char.codePointAt(0), 0) + 2) % 4); | |
| if (hostCheck < 2) { | |
| return true; | |
| } else { | |
| // Time bomb check | |
| if ((new window.Date()) >= (new window.Date('2025-09-01'))) { | |
| return true; | |
| } else { | |
| return false; | |
| } | |
| } | |
| } | |
| })(); | |
| if (isFreeTrial) { | |
| const watermark = document.createElement('a'); | |
| watermark.style = "position:fixed;display:block;z-index:100000;bottom:-100%;left:0;right:0;padding:20px 10px;color:#c7f9cc;background:rgba(0,0,0,0.6);font-family:Verdana;font-size:28px;font-weight:bold;line-height:1.5;letter-spacing:1px;text-align:center;text-decoration:none;box-shadow:0 0 5px #888;transition:bottom 3s;"; | |
| watermark.href = 'http://www.phpkobo.com/etobf?rev=r122-3216-1752463467'; | |
| watermark.target = '_blank'; | |
| watermark.innerHTML = "<div style='margin-bottom:-1px;font-family:Times New Roman;font-size:70%;font-style:italic;letter-spacing:4px;color:#80ed99;'>Thank you for evaluating</div><div>Elements Tab Obfuscator</div><div style='font-size:45%;color:#57cc99;'>Free Trial Version</div>"; | |
| window.setTimeout(function () { | |
| document.body.appendChild(watermark); | |
| window.setTimeout(function () { | |
| watermark.style.bottom = '0'; | |
| }, 1000); | |
| }, (window.Math.floor(window.Math.random() * 3 + 3)) * 1000); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment