   URLSearchParams = iframe.contentWindow.URLSearchParams;\n            Array = iframe.contentWindow.Array;\n            Object = iframe.contentWindow.Object;\n            atob = iframe.contentWindow.atob;\n            CustomEvent = iframe.contentWindow.CustomEvent;\n            performance = iframe.contentWindow.performance;\n            JSON = iframe.contentWindow.JSON;\n        } catch (error) {\n            // Empty catch block to prevent errors from showing in page logs\n        }\n    }\n\n    // If iframe was added successfully, remove it\n    if (iframe.parentNode) {\n        iframe.parentNode.removeChild(iframe);\n    }\n\n    /*\n     * If any of the values don\'t have the correct type, return and\n     * don\'t attempt any monkey patching. This avoids breaking web\n     * pages.\n     */\n    for (f of [\n        URL,\n        WeakMap,\n        WeakSet,\n        TextDecoder,\n        Uint8Array,\n        decodeURIComponent,\n        URLSearchParams,\n        Array,\n        Object,\n        atob,\n        CustomEvent,\n    ]) {\n        if (typeof f !== \'function\') {\n            return;\n        }\n    }\n\n    for (o of [performance, JSON]) {\n        if (typeof o !== \'object\') {\n            return;\n        }\n    }\n\n    /*\n     * Override common funcions that accept callbacks, such as\n     * setInterval, to keep track of what script each callback comes\n     * from. Skimmers commonly have the actual skimming code in a\n     * callback, so most of the time when we intercept a request,\n     * document.currentScript is null. To solve this, the script is\n     * stored in a closure when the callback is passed, and assigned\n     * to callbackCurrentscript when the callback is run.\n     */\n\n    let callbackCurrentScript = null;\n\n    // Maps callbacks originally passed to patched versions\n    const patchedCallbackMap = new WeakMap();\n\n    // Maps patched callbacks to original versions\n    const originalCallbackMap = new WeakMap();\n\n    /**\n     *  Generates the function with which to override the\n     *  callback-accepting functions.\n     *  @param realFn The real function, e.g. setInterval.\n     *  @param callbackIndex The index of the callback in the\n     *  arguments of realFn. For example, the callback is the first\n     *  argument in setInterval, so callbackIndex should be 0.\n     */\n    function generateAddCallbackFunction(realFn, callbackIndex) {\n        const patched = function(...args) {\n            const script = document.currentScript || callbackCurrentScript;\n\n            const callback = args[callbackIndex];\n\n            // If the callback isn\'t a function, there\'s nothing to patch\n            if (typeof callback !== \'function\') {\n                return realFn.apply(this, args);\n            }\n\n            // If a patch exists, use it instead of creating new function\n            let patchedCallback = patchedCallbackMap.get(callback);\n            if (!patchedCallback) {\n                patchedCallback = function() {\n                    callbackCurrentScript = script;\n                    const retVal = callback.apply(this, [...arguments]);\n                    callbackCurrentScript = null;\n                    return retVal;\n                };\n                patchedCallbackMap.set(callback, patchedCallback);\n                originalCallbackMap.set(patchedCallback, callback);\n            }\n\n            args[callbackIndex] = patchedCallback;\n            return realFn.apply(this, args);\n        };\n        // make toString() look native, #34\n        patched.toString = () => realFn.toString();\n        return patched;\n    }\n\n    const realSetInterval = window.setInterval;\n\n    window.setInterval = generateAddCallbackFunction(realSetInterval, 0);\n\n    const realSetTimeout = window.setTimeout;\n\n    window.setTimeout = generateAddCallbackFunction(realSetTimeout, 0);\n\n    const realPromiseThen = Promise.prototype.then;\n    Promise.prototype.then = generateAddCallbackFunction(realPromiseThen, 0);\n\n    const realPromiseCatch = Promise.prototype.catch;\n    Promise.prototype.catch = generateAddCallbackFunction(realPromiseCatch, 0);\n\n    const realPromiseFinally = Promise.prototype.finally;\n    if (realPromiseFinally) {\n        Promise.prototype.finally = generateAddCallbackFunction(\n            realPromiseFinally,\n            0\n        );\n    }\n\n    const realAddEventListener = EventTarget.prototype.addEventListener;\n\n    EventTarget.prototype.addEventListener = generateAddCallbackFunction(\n        realAddEventListener,\n        1\n    );\n\n    /*\n     * removeEventListener needs to be patched to map the passed\n     * listener to the patched listener.\n     */\n    const realRemoveEventListener = EventTarget.prototype.removeEventListener;\n    EventTarget.prototype.removeEventListener = function(...args) {\n        args[1] = patchedCallbackMap.get(args[1]) || args[1];\n\n        return realRemoveEventListener.apply(this, args);\n    };\n    EventTarget.prototype.removeEventListener.toString = () =>\n        realRemoveEventListener.toString();\n\n    /*\n     * Event listeners are sometimes set by directly assigning to a\n     * property from the GlobalEventHandlers mixin,\n     * e.g. \'submitBtn.onclick\'. For each of the GlobalEventHandlers\n     * implementations that are likely to be relevant, the event\n     * properties likely to be used by skimmers are overridden with\n     * our version that tracks currentScript when assigned.\n     */\n    for (const eventTarget of [\n        HTMLElement.prototype,\n        Document.prototype,\n        window,\n    ]) {\n        for (const eventName of [\n            \'onclick\',\n            \'onblur\',\n            \'onfocus\',\n            \'onchange\',\n            \'oninput\',\n            \'onkeydown\',\n            \'onkeypress\',\n            \'onkeyup\',\n            \'onload\',\n            \'onloadend\',\n            \'onmousedown\',\n            \'onmouseenter\',\n            \'onmouseleave\',\n            \'onmousemove\',\n            \'onmouseout\',\n            \'onmouseover\',\n            \'onmouseup\',\n            \'onpointerdown\',\n            \'onpointermove\',\n            \'onpointerup\',\n            \'onpointerover\',\n            \'onpointerout\',\n            \'onpointerenter\',\n            \'onpointerleave\',\n            \'onsubmit\',\n        ]) {\n            const descriptor = Object.getOwnPropertyDescriptor(\n                eventTarget,\n                eventName\n            );\n\n            if (!descriptor) {\n                continue;\n            }\n\n            const realGetter = descriptor.get;\n            const realSetter = descriptor.set;\n\n            const patchedSetter = generateAddCallbackFunction(realSetter, 0);\n\n            /*\n             * In case a script checks the value of the property after\n             * setting it, the getter has to be patched to return the\n             * original callback.\n             */\n            const patchedGetter = function() {\n                const realValue = realGetter.call(this);\n\n                const originalCallback = originalCallbackMap.get(realValue);\n                if (originalCallback) {\n                    return originalCallback;\n                } else {\n                    return realValue;\n                }\n            };\n\n            patchedGetter.toString = () => realGetter.toString();\n\n            Object.defineProperty(eventTarget, eventName, {\n                get: patchedGetter,\n                set: patchedSetter,\n            });\n        }\n    }\n\n    /*\n     * This script has some dependencies that need to be included\n     * without polluting the global namespace. This is achieved by\n     * inserting them into the source at build time in the\n     * makefile. This is preferable to loading the dependencies via\n     * AJAX, because the latter doesn\'t work if the CSP disallows\n     * unsafe-eval. The following tag tells the makefile where to\n     * insert the dependencies.\n     */\n    // <DEPENDENCIES>\nclass InputAnalyser {\n    constructor(_inputs) {\n        this._inputs = _inputs;\n        this._irrelevantInputs = new WeakSet();\n\n        /*\n         * Not currently using Password because it was causing too\n         * many false positives. Some sites use external auth domains,\n         * and many have misconfigured analytics that collect\n         * passwords.\n         */\n        this.relevantInputs = [new CardNumber()];\n    }\n\n    get relevantInputValues() {\n        const inputs = {};\n        this._inputs.forEach(\n            input => (inputs[input.id || input.name] = input.value)\n        );\n\n        return inputs;\n    }\n\n    get relevantFilledInputs() {\n        return this.allInputs\n            .filter(input => this.isRelevantElement(input))\n            .filter(input => input.value.length >= cons.minInputSize)\n            .map(input => ({\n                id: input.id,\n                name: input.name,\n                value: input.value,\n            }));\n    }\n\n    get allInputs() {\n        this._inputs = [\n            ...document.querySelectorAll(\n                this.relevantInputs.map(i => i.relevantInputTypes).join(\', \')\n            ),\n        ];\n\n        return this._inputs;\n    }\n\n    static getLabelText(input) {\n        if (!input.labels.length) {\n            return \'\';\n        }\n        return stripSpecialChars(input.labels[0].textContent).trim();\n    }\n\n    /**\n     * Takes an HTMLElement and determines whether or not it could be relevant for the purposes of\n     * skimmer detection.\n     * @param {HTMLElement} input The element which is to be checked for relevancy.\n     * @returns {boolean} Returns true if the element is potentially relevant (e.g. a credit card\n     * field or other such sensitive input).\n     */\n    isRelevantElement(input) {\n        // If the element has been identified as irrelevant, return false\n        if (this._irrelevantInputs.has(input)) {\n            return false;\n        }\n\n        // Check the input against all the relevant input types\n        const relevances = this.relevantInputs.map(i => i.test(input));\n\n        /*\n         *  If they all agree that this input is unlikely to become\n         *  relevant, exclude it from further checks and return false\n         */\n        if (relevances.every(i => i === RelevantInput.relevance.NOT_RELEVANT)) {\n            this._irrelevantInputs.add(input);\n            return false;\n        }\n\n        // Return true if any consider the input to be relevant, otherwise false\n        return relevances.some(i => i === RelevantInput.relevance.RELEVANT);\n    }\n}\n/*\n * Input Matcher represents a class that can have matches with relevant inputs.\n * Each class that inherits must implement the findInputMatches method.\n */\nclass InputMatcher {\n    /*\n     * Finds any matches with the list of inputs.\n     * @param inputs. An object whose values represent relevant inputs.\n     * @return {[String]} The keys of the inputs that were found.\n     */\n    findInputMatches(inputs) {\n        throw new Error(\'findInputMatches has not been implemented.\');\n    }\n\n    /**\n     * Determines whether one or more values in needles are present in any of\n     * the keys of the haystacks.\n     * Checks both raw input and base64 encoded versions of the needles.\n     * @param needles An object. The haystacks are searched for the values of\n     * this object.\n     * @param haystacks The array of strings in which the needle(s) may reside.\n     * A string may optionally be supplied if only one value is present.\n     * @returns {[String]} The keys of the needles that were found in one or\n     * more haystacks in either raw or base64 encoded form.\n     */\n    static atLeastOneNeedleInHaystack(needles, haystacks) {\n        if (\n            needles === undefined ||\n            needles.length === 0 ||\n            haystacks === undefined\n        ) {\n            return [];\n        }\n\n        const searchArray = Array.isArray(haystacks) ? haystacks : [haystacks];\n\n        return Object.entries(needles)\n            .filter(([_, needle]) =>\n                searchArray.some(haystack => {\n                    if (isBase64Encoded(haystack)) {\n                        if (atob(haystack).indexOf(needle) !== -1) {\n                            return true;\n                        }\n                    }\n                    return haystack.indexOf(needle) !== -1;\n                })\n            )\n            .map(([input, _]) => input);\n    }\n}\n/*\n * Matcher for an object that cannot contain any inputs.\n */\nclass EmptyMatcher extends InputMatcher {\n    findInputMatches(inputs) {\n        return [];\n    }\n}\n\nclass URLParamsMatcher extends InputMatcher {\n    constructor(url) {\n        super();\n        this._url = url;\n    }\n\n    findInputMatches(inputs) {\n        try {\n            const parsedUrl = new URL(this._url);\n            return InputMatcher.atLeastOneNeedleInHaystack(inputs, [\n                ...parsedUrl.searchParams.values(),\n            ]);\n        } catch (e) {\n            return [];\n        }\n    }\n}\n\nclass StringBodyMatcher extends InputMatcher {\n    constructor(str) {\n        super();\n        this._str = str;\n    }\n\n    findInputMatches(inputs) {\n        let decoded;\n        try {\n            decoded = decodeURIComponent(this._str);\n        } catch (e) {\n            decoded = this._str;\n        }\n\n        const searchParams = new URLSearchParams(decoded);\n        const parsedQuery = {};\n        const parsedValues = [];\n\n        for (const [key, value] of searchParams.entries()) {\n            parsedQuery[key] = value;\n            parsedValues.push(value);\n        }\n\n        if (parsedQuery !== {} && parsedValues.indexOf(null) === -1) {\n            return InputMatcher.atLeastOneNeedleInHaystack(\n                inputs,\n                parsedValues\n            );\n        }\n        return InputMatcher.atLeastOneNeedleInHaystack(inputs, decoded);\n    }\n}\n\nclass FormBodyMatcher extends InputMatcher {\n    constructor(formData) {\n        super();\n        this._formData = formData;\n    }\n\n    findInputMatches(inputs) {\n        return InputMatcher.atLeastOneNeedleInHaystack(\n            inputs,\n            Object.values(this._formData).flat()\n        );\n    }\n}\n/**\r\n * Takes a string and removes a variety of \'special\' characters like punctuation.\r\n * @param str The string to be modified.\r\n * @returns {string} The resulting string without any special characters.\r\n */\r\nfunction stripSpecialChars(str) {\r\n    return str.replace(/[`~!@#$%^&*()_|+\\-=?;:\'\",.<>}{\\][\\\\/]/gi, \'\');\r\n}\r\n\r\n/**\r\n * Attempts to identify where a string is base-64 encoded or not.\r\n * @param string The raw string to be examined.\r\n * @returns {boolean} Returns true if the string matches a base-64 string. False otherwise.\r\n */\r\nfunction isBase64Encoded(string) {\r\n    return /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/.test(\r\n        string\r\n    );\r\n}\r\n\r\n// Computes the base64 sha512/sha224 hash of some text, removing trailing ==\r\nfunction base64hash(hashFunction, text) {\r\n    return btoa(\r\n        hashFunction\r\n            .array(text)\r\n            .map(char => String.fromCharCode(char))\r\n            .join(\'\')\r\n    ).slice(0, -2);\r\n}\r\n\r\nfunction normaliseUrl(str) {\r\n    const link = document.createElement(\'a\');\r\n    link.href = str;\r\n    return link.href;\r\n}\r\n\r\nfunction stripCredentials(urlstr) {\r\n    try {\r\n        const url = new URL(urlstr);\r\n        const params = url.searchParams;\r\n        params.forEach((_, key) => params.set(key, \'\'));\r\n        return url.toString();\r\n    } catch (e) {\r\n        return urlstr;\r\n    }\r\n}\r\n\r\n/**\r\n * Check if two URLs match regardless of any partial encoding.\r\n *\r\n * Uses decodeURI which can throw a URIError when url is malformed.\r\n */\r\nfunction urlsMatch(first, second) {\r\n    try {\r\n        return decodeURI(first) === decodeURI(second);\r\n    } catch (e) {\r\n        return false;\r\n    }\r\n}\r\n\r\nfunction sendMessage(type, data, tabId, options) {\r\n    if (tabId) {\r\n        chrome.tabs.sendMessage(tabId, { data, type }, options);\r\n    } else {\r\n        chrome.runtime.sendMessage({ data, type });\r\n    }\r\n}\r\n\r\n// Encodes the given input using base64url as defined by RFC4648 section 5.\r\nfunction base64Url(input) {\r\n    return btoa(input)\r\n        .replace(\'+\', \'-\')\r\n        .replace(\'/\', \'_\');\r\n}\r\n/*\n * Netcraft Extension\n * constants JavaScript\n */\n\n/* eslint sort-keys: \'off\' */\n/* sort-keys is disabled as the constants are separated by type */\n\n// Aligned values here make for easier reading, so we disable prettier\n\n// prettier-ignore\nconst cons = (function() {\n    const cryptojackersUrl = \'https://www.netcraft.com/apps/help/glossary/#crypto-miners\';\n    const skimmersUrl = \'https://www.netcraft.com/apps/help/glossary/#credit-card-skimmers\';\n    const evilResourceCrime = \'attempted to load\';\n\n    return {\n        fields: [\n            \'topsites\',\n            \'country\',\n            \'netblock\',\n            \'hoster\',\n            \'firstseen\',\n            \'rank\',\n            \'risk\',\n            \'uses_sslv3\',\n            \'pfs\',\n            \'heartbleed\',\n            \'heartbleed_message\',\n        ],\n\n        // options that can be toggle in options.js and installed flag\n\n        toggleOptions: [\n            \'installed\',\n            \'block\',\n            \'xss\',\n            \'suspicious\',\n            \'other-malicious-scripts\',\n            \'credential-leaks\',\n            \'analytics\',\n            \'cryptojackers\',\n            \'skimmers\',\n        ],\n\n        // domains\n        mirror:  \'https://mirror2.extension.netcraft.com/\',\n        toolbar: \'https://mirror.toolbar.netcraft.com/\',\n\n        // links and faqs\n        blogArchive:      \'https://news.netcraft.com/archives/\',\n        browserApp:       \'https://www.netcraft.com/apps/browser/\',\n        cryptojackersUrl,\n        extensionStore:   \'https://addons.mozilla.org/en-us/firefox/addon/netcraft-toolbar?src=external-blocked_visit-rating\',\n        heartbleedUrl:    \'https://www.netcraft.com/apps/help/glossary/#heartbleed\',\n        logBlockedVisit:  \'https://toolbar.netcraft.com/blocked_visit\',\n        netblock:         \'https://sitereport.netcraft.com/netblock?q=\',\n        pfsUrl:           \'https://www.netcraft.com/apps/help/glossary/#pfs\',\n        report:           \'https://sitereport.netcraft.com/?url=\',\n        reportAPI:        \'https://report.netcraft.com/api/v2/report/urls\',\n        reportMistake:    \'https://report.netcraft.com/report/mistake?url=\',\n        skimmersUrl,\n        sslv3Url:         \'https://www.netcraft.com/apps/help/glossary/#sslv3\',\n        submissionURL:    \'https://report.netcraft.com/submission/\',\n        topsites:         \'https://trends.netcraft.com/topsites?s=\',\n\n        // paths\n        block:              \'blocked.html\',\n        check:              \'check_url/v4/\',\n        danger:             \'Images/danger.png\',\n        embed:              \'/embedded\',\n        evilJSPatternsFeed: \'eviljspatterns\',\n        skimmerDomainWhitelistFeed: \'eviljsskimmerwhitelist\',\n        evilResourcesFeed:  \'blockdb/netcraft_extension_evil_resource_encrypted-yppL7bVh/\',\n        flags:              \'Images/flags/\',\n        force:              \'/dodns\',\n        heartbleed:         \'Images/heartbleed.png\',\n        info:               \'/info\',\n        poodlePost:         \'2014/10/15/googles-poodle-affects-oodles.html\',\n        warning:            \'Images/warning.png\',\n\n        // feed constants\n        evilResourceTypes: [\'cryptojacker\', \'skimmer\'],\n        evilResourcesSalt: \'C4jhYHxW\',\n        maxRevisions:      5,\n        randomSaltLength:  8,\n\n        // mappings of url_type => information\n        urlTypeInformationMappings: {\n            \'malicious_javascript/cryptojacker\': [\'web miners\',             cryptojackersUrl],\n            \'malicious_javascript/skimmer\':      [\'shopping site skimmers\', skimmersUrl],\n        },\n\n        // mappings of evilResourceTypes => user options\n        evilResourceOptionMappings: {\n            \'cryptojacker\': \'cryptojackers\',\n            \'skimmer\':      \'skimmers\'\n        },\n\n        // mappings of user options => url_type\n        optionUrlTypeMappings: {\n            \'block\':                   /^(?!malicious_javascript:)/,\n            \'cryptojackers\':           /^malicious_javascript\\/cryptojacker$/,\n            \'other-malicious-scripts\': /^malicious_javascript\\/(?!(skimmer|cryptojacker)$)/,\n            \'skimmers\':                /^malicious_javascript\\/skimmer$/,\n        },\n\n        // report constants\n        malwareSourceEmail:  \'extension-reported-malware@netcraft.com\',\n        malwareSourceUUID:   \'mwcCcApi89cobY9loJ9rWJ9HuOhlpwMj\',\n        extensionSourceUUID: \'qBEQ3NhEnwK4sypmuSxBJzRqpxZLRXxe\',\n\n        // report error default message\n        reportErrorMessage: \'An error occured with your submission. Please try again.\',\n\n        // mapping from report API errors to displayed messages\n        reportErrorMappings: {\n            \'Duplicate of a recent submission\': \'This URL has already been reported.\',\n            \'Supplied email is not valid\': \'Invalid email address. Please correct it and try again.\',\n        },\n\n        whitelistErrorMessages: {\n            invalidHostname: \'Invalid hostname, please correct it and try again\',\n            duplicate: \'Protection is already disabled for this site\',\n        },\n\n        // browserAction icon paths\n        iconNormal: {\n            16: \'Images/action-normal-16.png\',\n            24: \'Images/action-normal-24.png\',\n            32: \'Images/action-normal-32.png\'\n        },\n        iconDanger: {\n            16: \'Images/action-danger-16.png\',\n            24: \'Images/action-danger-24.png\',\n            32: \'Images/action-danger-32.png\'\n        },\n        iconWarning: {\n            16: \'Images/action-warning-16.png\',\n            24: \'Images/action-warning-24.png\',\n            32: \'Images/action-warning-32.png\'\n        },\n        iconDisabled: {\n            16: \'Images/action-disabled-16.png\',\n            24: \'Images/action-disabled-24.png\',\n            32: \'Images/action-disabled-32.png\'\n        },\n\n        // Extension internal message types\n        capturedIntercept:  \'interceptDetailsCaptured\',\n        confirmedIntercept: \'interceptDetailsConfirmed\',\n        contentReady:       \'contentScriptReady\',\n        leaksEnabled:       \'credentialLeaksEnabled\',\n        reportURL:          \'report\',\n        reqBlock:           \'block\',\n        reqDetails:         \'details\',\n        reqForce:           \'force\',\n        reqPopUp:           \'popup\',\n        reqPrintTimingData: \'time\', //debug\n        reqInputs:          \'requestInputValues\',\n        sendInputs:         \'sendInputs\',\n        sendIntercept:      \'sendInterceptDetails\',\n        updateOption:       \'updateOption\',\n        updateWhitelist:    \'whitelist\', // disable button whitelist\n\n        // Status codes\n        errEvilResource: \'EVILRESOURCE\',\n        errNoDetails:    \'NODETAILS\',\n        errNoInfo:       \'NOINFO\',\n        errBlocked:      \'BLOCKED\',\n        errPending:      \'PENDING\',\n        errServer:       \'ERROR\',\n        errSuspicious:   \'SUSPICIOUS\',\n        errTimeout:      \'TIMEOUT\',\n        errXSS:          \'XSS\',\n        noError:         \'OK\',\n\n        // Valid protocols (it is a regex match so ^ matches start of string)\n        protocols:    [\'^http\', \'^https\', \'^ftp\'],\n\n        // Human readable messages\n        msgNa:           \'NA\',\n        msgNewSite:      \'New Site\',\n        msgNoInfo:       \'Site information not available\',\n        msgNoServer:     \'Unable to contact Netcraft servers\',\n        msgSuspicious:   \'Suspicious URL Detected\',\n        msgXSS:          \'Suspected XSS Attack\',\n        msgBlocked:      {\n            \'malicious_javascript/skimmer\': {\n                title: \'Shopping Site Skimmer Detected\',\n                description: \'shopping site skimmer\'\n            },\n            \'malicious_javascript/dropsite\': {\n                title: \'Potential Credential Leak Detected\',\n                description: \'potential credential leak\'\n            },\n            \'malicious_javascript/cryptojacker\': {\n                title: \'Web Miner Detected\',\n                description: \'web miner\'\n            },\n            \'malware_c2/infrastructure\': {\n                title: \'Malicious URL\',\n                description: \'malicious page\'\n            },\n            \'default\': {\n                title: \'Suspected Phishing\',\n                description: \'suspected phishing site\'\n            },\n        },\n\n        // Human readable reasons\n        urlTypeCrimeMappings: {\n            \'malicious_javascript/skimmer\':      evilResourceCrime,\n            \'malicious_javascript/cryptojacker\': evilResourceCrime,\n            \'malicious_javascript/dropsite\':     \' is blocked because we detected that sensitive information was leaked from\',\n        },\n\n        // Timeout function value to clear cache etc.\n        timeout:      300000,\n\n        // Skimmer whitelist update interval (1 day)\n        skimmerWhitelistInterval: 1000 * 60 * 60 * 24,\n\n        // Google Analytics account\n        account:      \'UA-2150242-8\',\n        gaCategory:   \'Popup load times\',\n\n        // InputAnalyser constants\n        minInputSize: 5,\n\n        // First addresses of the private IPv4 netblocks\n        privateIPv4Blocks: [\n            \'10.0.0.0\',\n            \'172.16.0.0\',\n            \'192.168.0.0\'\n        ],\n\n        // Sync storage key for disable for this site button whitelist\n        disableButtonWhitelistKey: \'disableButtonWhitelist\',\n\n        // Local storage key for local credential leak detection whitelist\n        localCredentialLeakWhitelistKey: \'localCredentialLeakWhitelist\',\n\n        // Whitelisted fileName regexes for credential leak detection\n        credentialLeakFileNameWhitelist: [\n            /^https?:\\/\\/googleads.g.doubleclick.net\\//,\n        ],\n    };\n})();\n\n/*\n * recursively prevents any property or sub-property in cons from:\n *   - being added;\n *   - being removed; or\n *   - being modified.\n */\nfunction deepFreeze(object) {\n    Object.freeze(object);\n\n    for (const name of Object.getOwnPropertyNames(object)) {\n        if (typeof object[name] === \'object\') {\n            deepFreeze(object[name]);\n        }\n    }\n}\n\ndeepFreeze(cons);\n/*\r\n * RelevantInput objects check whether given form inputs should be\r\n * treated as sensitive data.\r\n *\r\n * This should be treated as an abstract class. Only the subclasses\r\n * are intended to be instantiated.\r\n */\r\nclass RelevantInput {\r\n    /*\r\n     * Enum of the possible return values of test():\r\n     * RELEVANT:\r\n     *     Input is relevant now, outgoing requests should be\r\n     *     checked for the contained value (subject to minimum length).\r\n     * POTENTIALLY RELEVANT:\r\n     *     Input should not be considered relevant now, but may be in\r\n     *     future so should continue to be checked.\r\n     * NOT_RELEVANT:\r\n     *     Input is not relevant and will not become relevant, so can\r\n     *     be ommitted from further checks to optimise performance.\r\n     */\r\n    static get relevance() {\r\n        return {\r\n            NOT_RELEVANT: 1,\r\n            POTENTIALLY_RELEVANT: 2,\r\n            RELEVANT: 3,\r\n        };\r\n    }\r\n\r\n    /*\r\n     * relevantInputTypes (string) - a comma separated list of\r\n     * selectors that describe the types of input that should be\r\n     * considered relevant.\r\n     */\r\n    constructor(relevantInputTypes) {\r\n        this.relevantInputTypes = relevantInputTypes;\r\n    }\r\n\r\n    /*\r\n     * Checks whether any of the given input\'s \'labels\' (the ID, name,\r\n     * or actual label) match the given regex.\r\n     */\r\n    static testInputLabel(regex, input) {\r\n        const labels = [\r\n            input.id,\r\n            input.name,\r\n            InputAnalyser.getLabelText(input),\r\n        ];\r\n        return labels.some(label => regex.test(label));\r\n    }\r\n}\r\n\r\n/*\r\n * RelevantInput for passwords.\r\n */\r\nclass Password extends RelevantInput {\r\n    constructor() {\r\n        super(\'input[type=password]\');\r\n    }\r\n\r\n    /*\r\n     * Passwords are verified purely by their input type.\r\n     */\r\n    test(input) {\r\n        if (input.matches(this.relevantInputTypes)) {\r\n            return RelevantInput.relevance.RELEVANT;\r\n        } else {\r\n            return RelevantInput.relevance.NOT_RELEVANT;\r\n        }\r\n    }\r\n}\r\n\r\n/*\r\n * RelevantInput for card numbers.\r\n */\r\nclass CardNumber extends RelevantInput {\r\n    constructor() {\r\n        // Inputs with no type attribute act like text inputs\r\n        super(\r\n            \'input[type=text], input[type=tel], input[type=number], input:not([type])\'\r\n        );\r\n\r\n        // Definitely card number labels\r\n        this.inputLabelRegexes = [\r\n            /(cc|card).?(num|no)/i,\r\n            /カード番号/,\r\n            /Номер.*карты/i,\r\n            /信用卡卡號/i,\r\n            /信用卡号码/i,\r\n        ];\r\n\r\n        /*\r\n         * Matches incomplete card numbers, because ideally the\r\n         * skimmer should be detected before the victim gives away\r\n         * most of their card number.\r\n         */\r\n        this.inputValueRegexes = [/^(\\d{4}[-\\s]?){0,4}\\d{1,4}$/];\r\n\r\n        // Probably card number labels\r\n        this.suspectInputLabelRegexes = [\r\n            /card/i,\r\n            /tarjeta/i,\r\n            /carte/i,\r\n            /карты/i,\r\n            /卡/i,\r\n            /カード/i,\r\n            /카드/i,\r\n            /credit[^s]/i,\r\n        ];\r\n\r\n        // If it matches one of these, probably not a card number\r\n        this.inputLabelExceptionRegexes = [\r\n            /post/i,\r\n            /code/i,\r\n            /zip/i,\r\n            /acc(oun)?t?[-_]?n(um|o)/i,\r\n            /credit[-_]?(sum|limit)/i,\r\n            /loyalty/i,\r\n            /gift/i,\r\n            /idcard/i,\r\n            /cardid/i,\r\n            /club/i,\r\n            /sku/i,\r\n            /(\\b|_)otp/i,\r\n            /price/i,\r\n            /amount/i,\r\n            /service/i,\r\n            /picard/i,\r\n            /search/i,\r\n            /contact/i,\r\n            /callme/i,\r\n            /chat/i,\r\n            /activate/i,\r\n            /msisdn/i,\r\n        ];\r\n    }\r\n\r\n    test(input) {\r\n        if (!input.matches(this.relevantInputTypes)) {\r\n            return RelevantInput.relevance.NOT_RELEVANT;\r\n        }\r\n\r\n        // Return NOT_RELEVANT if label matches an exception regex\r\n        if (\r\n            this.inputLabelExceptionRegexes.some(r =>\r\n                RelevantInput.testInputLabel(r, input)\r\n            )\r\n        ) {\r\n            return RelevantInput.relevance.NOT_RELEVANT;\r\n        }\r\n\r\n        // Return RELEVANT if label clearly states card number\r\n        if (\r\n            this.inputLabelRegexes.some(r =>\r\n                RelevantInput.testInputLabel(r, input)\r\n            )\r\n        ) {\r\n            return RelevantInput.relevance.RELEVANT;\r\n        }\r\n\r\n        /*\r\n         * If label suggests card number, return RELEVANT if value\r\n         * also suggests card number or POTENTIALLY_RELEVANT if\r\n         * not. Otherwise, return NOT_RELEVANT as this element will\r\n         * not become relevant unless a label changes (unlikely).\r\n         */\r\n        if (\r\n            this.suspectInputLabelRegexes.some(r =>\r\n                RelevantInput.testInputLabel(r, input)\r\n            )\r\n        ) {\r\n            if (this.inputValueRegexes.some(r => r.test(input.value))) {\r\n                return RelevantInput.relevance.RELEVANT;\r\n            } else {\r\n                return RelevantInput.relevance.POTENTIALLY_RELEVANT;\r\n            }\r\n        } else {\r\n            return RelevantInput.relevance.NOT_RELEVANT;\r\n        }\r\n    }\r\n}\r\n\n    const inputAnalyser = new InputAnalyser();\n\n    const resourceBlockList = new Set([\n                            \"6C590F97DDB1A11AE795B3050E4C3866\", \"D285BE79786E2ECE396941331C5BA65E\", \"59DF8523520DE7F08FB53D3D12BD94F5\", \"1FB784EBAFDA1618023756BF5B6D7E46\", \"0D6E1B5F03B81F5C37813B22E29C4FE2\", \"EFAB0ECE9383D50C555B8168C3E2A4DD\", \"AC8A4B13A411A4E43E61D0A034FA4A55\", \"25A781F6D1F94EEACCF1E00E844D9E14\", \"3920B04ED0D215EA738F524F60485F23\", \"05F4A2D3161A92EB82A44F15FC2A3AEC\", \"C5A1BE7B3855B0A25275B2877E005CB9\", \"00069CEC347076721EBE2257978A0A33\", \"9A513130D02F360BBDC16D8AC0C579D8\", \"7CDB2A3A29E50F90C1000252EF523AD6\", \"25CE912FAF2376026AAD0174AD5A0329\", \"739E1F67AA5F64A06FC469F6030802D4\", \"F49368C9A05C29EF49411999C5987F2C\", \"AB2DD89FB892155275D8780CF527BC93\", \"CFE758EB18FBFE70B1F690241B75D2B1\", \"E43C3463B40A790545F4149FF06B419E\", \"8B6D4C76A7CCF5EB720660B8B2D3F5EB\", \"45573B3E2F613AF5BF2145DF004BABD1\", \"E3DB4E968B371DE94CE607199A062F39\", \"A4EFF1240C5DE614B394FB738CBD28E2\", \"F42D10E84B9A69A8A24062319F425A83\", \"90646BE39982594A58EDF5D2630CCFFD\", \"669853BEA9B84EA51D7302A7FBFF5BC3\", \"0BA566B4777526F451108100107DC5ED\", \"AB19E12BFFBC47244BDCC2403EAA9037\", \"61DAF8537240639277FD1A5908A131A4\", \"6F1DC1309CFE734C038685D8F17A3B6B\", \"DCC3DB001334F29C1C0208D15E9EAB49\", \"2DBD7D54022C28273ADE3147B8EEB076\", \"AE3760E93412C65EC5BF63537B037019\", \"4CD96A830DAC0E0B5EBE578B0DD870AE\", \"4C70569BBE1821F5655382660807FA77\", \"05ECA17FF0EA78669ED43B2BD7B63571\", \"44E503B3621513B9D3666F284E4DF47A\", \"8F1C92E7665860AE70CBB228DED86111\", \"D69552885B55C4097E5725F805815639\", \"B4357A9D889E974CDF3F9740E48F9A1B\", \"5709DC60CFC365B7064B06C709A9D32B\", \"14E8729CBB16AC3625EAA0D7A679E56E\", \"793D4DABAAA377A12F2B6CE88C5FB389\", \"C5D1DCE2AE0D8CE79054FC16CB13D65A\", \"89F91750BD4A9397D1F8CE183757FF70\", \"50B8BC213FB188A687173C1A0A77EA57\"\n                        ]);\n    const evilJSPatterns = new Set([\n                            \n                        ]);\n\n    // Extract body object\n    function extractBody(data) {\n        if (!data) {\n            return Promise.resolve({});\n        }\n        if (FormData.prototype.isPrototypeOf(data)) {\n            const formData = {};\n            for (const [key, value] of data.entries()) {\n                formData[key] = value;\n            }\n            return Promise.resolve({ formData });\n        }\n        // Create a temporary Request object to parse the body\n        const tempRequest = new Request(\'\', { body: data, method: \'POST\' });\n        return extractBodyFromRequest(tempRequest);\n    }\n\n    function extractBodyFromRequest(request) {\n        // Try to extract as text\n        return request\n            .text()\n            .then(text => {\n                return { text };\n            })\n            .catch(e => {\n                return {};\n            });\n    }\n\n    /**\n     * This function checks if the request hostname/URL is in the blocklist.\n     * Used when identifying whether or not to forward request to background\n     * script.\n     * @param hostname Hostname to check against blocklist.\n     * @param url Request url to check against evil JS patterns\n     * @returns boolean true if hostname is in the blocklist, false if\n     * it is not in blocklist or if blocklist is not provided.\n     */\n    function requestInBlocklist(hostname, url) {\n        if (resourceBlockList === undefined || evilJSPatterns === undefined) {\n            return false;\n        }\n        if (testEvilJS(url.replace(/^(https?:)?\\/\\//i, \'\'))) {\n            return true;\n        }\n        while (!testEvilJS(hostname) && !testResource(hostname)) {\n            const newHostname = hostname.replace(/^.+?\\./g, \'\');\n\n            if (hostname === newHostname) return false;\n\n            hostname = newHostname;\n        }\n        return true;\n    }\n\n    function testEvilJS(url) {\n        return evilJSPatterns.has(base64hash(sha512, url));\n    }\n\n    function testResource(hostname) {\n        return resourceBlockList.has(\n            md5(cons.evilResourcesSalt + hostname).toUpperCase()\n        );\n    }\n\n    /*\n     * Implementations to overwrite window.XMLHttpRequest.prototype.open,\n     * window.XMLHttpRequest.prototype.send and window.fetch. These are (to my knowledge) the two\n     * APIs that can be used to make requests within Javascript. If there are more, then they will\n     * also need to be overridden and intercepted like below.\n     */\n\n    /**\n     * This function generates functions to be used for wrapping and intercepting network\n     * requests, namely from XMLHttpRequest and the Fetch API. It works by taking the real\n     * function we want to intercept the request for, and then uses document.currentScript to work\n     * out which file initiated the request. It sends those details to the content script which\n     * forwards it to the background script, and then we await a confirmation of receipt from the\n     * background page. Finally, the real request is sent by forwarding parameters to the original\n     * function passed in.\n     * @param realFn A reference to the real function that is being intercepted.\n     * @param getRequestUrlFn A function used to get the request URL from the object. This is\n     * necessary as the mechanisms for different objects differs depending on implementation.\n     * @param getRequestBodyPr A function used to get the request body from the object. Returns\n     * a promise. It should be null if it\'s not applicable (e.g. if it sets the src of an image or script).\n     * @param type The type of request, which will be a ResourceType:\n     * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType\n     * @returns {function(...[*]=): PromiseLike<any | never>}\n     */\n    function generateRequestInterceptFn(\n        realFn,\n        getRequestUrlFn,\n        getRequestBodyPr,\n        type\n    ) {\n        const patched = function(...args) {\n            /*\n             * We create a link DOM element in order to correctly generate the\n             * absolute path of the request, automatically transforming from a\n             * relative path if necessary.\n             */\n            const requestUrl = normaliseUrl(\n                getRequestUrlFn.bind(this)(...args)\n            );\n\n            let requestHostname;\n            let pageHostname;\n\n            try {\n                requestHostname = new URL(requestUrl).hostname;\n                pageHostname = new URL(location.href).hostname;\n            } catch (e) {\n                return realFn.apply(this, args);\n            }\n\n            /*\n             * If hostname is in the blocklist, the request should always be\n             * sent to the background script.\n             */\n            // eslint-disable-next-line prefer-const\n            let forceStall = false;\n\n            if (!forceStall) {\n                /*\n                 * Delaying outgoing requests sometimes causes problems\n                 * with elements such as video players and dynamically\n                 * loaded images, so it should only be done when\n                 * necessary. Before inputAnalyser is defined and when the\n                 * request is to the same hostname as the page, this can\n                 * be skipped.\n                 */\n                if (\n                    requestHostname === pageHostname ||\n                    inputAnalyser === undefined\n                ) {\n                    return realFn.apply(this, args);\n                }\n\n                /*\n                 * It can also be skipped if there is no sensitive\n                 * information entered into a form on the page yet.\n                 */\n                const relevantFilledInputs = inputAnalyser.relevantFilledInputs;\n                if (!relevantFilledInputs.length) {\n                    return realFn.apply(this, args);\n                }\n\n                /*\n                 * For requests with no body, it\'s only necessary to delay\n                 * them when the sensitive information from the form\n                 * inputs is present in the request URL.\n                 */\n                const ia = new InputAnalyser(relevantFilledInputs);\n                const relevantInputValues = ia.relevantInputValues;\n                const urlMatcher = new URLParamsMatcher(requestUrl);\n\n                if (\n                    getRequestBodyPr === null &&\n                    !urlMatcher.findInputMatches(relevantInputValues).length\n                ) {\n                    return realFn.apply(this, args);\n                }\n            }\n\n            /*\n             * Create a promise that resolves immediately if the request body\n             * does not apply to this request.\n             */\n            if (getRequestBodyPr === null) {\n                getRequestBodyPr = function() {\n                    return Promise.resolve({});\n                };\n            }\n\n            let fileName;\n            if (document.currentScript && document.currentScript.src) {\n                fileName = document.currentScript.src;\n            } else if (callbackCurrentScript && callbackCurrentScript.src) {\n                fileName = callbackCurrentScript.src;\n            } else {\n                fileName = location.href;\n            }\n\n            return getRequestBodyPr\n                .bind(this)(...args)\n                .then(requestBody => {\n                    /*\n                     * Generate the information object to send through to the\n                     * content script including the request URL, the initiator\n                     * filename and a timestamp relative to the page load.\n                     */\n                    const detail = {\n                        fileName,\n                        requestBody,\n                        requestUrl,\n                        timestamp: performance.now(),\n                        type,\n                    };\n\n                    document.dispatchEvent(\n                        new CustomEvent(cons.capturedIntercept, {\n                            detail,\n                        })\n                    );\n\n                    return new Promise(resolve => {\n                        /*\n                         * Send the initiator\'s file name and other info to the\n                         * background script, then wait for confirmation that the\n                         * details made it to the background page before calling\n                         * the real function. This is to avoid any race conditions\n                         * whereby the details haven\'t been added to the tab data\n                         * before onBeforeRequest/onBeforeSendHeaders gets called.\n                         */\n                        document.addEventListener(\n                            cons.confirmedIntercept,\n                            function compareData(customEvent) {\n                                const receivedData = JSON.parse(\n                                    customEvent.detail\n                                );\n                                if (detailsMatch(detail, receivedData)) {\n                                    /*\n                                     * If it\'s the correct confirmation, then we\n                                     * can resolve the promise and call the real\n                                     * function in the finally block.\n                                     */\n                                    document.removeEventListener(\n                                        cons.confirmedIntercept,\n                                        compareData,\n                                        false\n                                    );\n                                    resolve();\n                                }\n                            }\n                        );\n                    }).then(() => realFn.apply(this, args));\n                });\n        };\n        // make toString() look native, #34\n        patched.toString = () => realFn.toString();\n        return patched;\n    }\n\n    function detailsMatch(first, second) {\n        const comparisonKeys = [\'fileName\', \'requestUrl\', \'timestamp\', \'type\'];\n        for (const key of comparisonKeys) {\n            if (first[key] !== second[key]) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    window.fetch = generateRequestInterceptFn(\n        realFetch,\n        request => (typeof request === \'string\' ? request : request.url),\n        (request, options) => {\n            // Check if argument is a Request object.\n            if (request instanceof Request) {\n                return extractBodyFromRequest(request);\n            }\n            return options ? extractBody(options.body) : Promise.resolve({});\n        },\n        \'xmlhttprequest\'\n    );\n\n    window.XMLHttpRequest.prototype.send = generateRequestInterceptFn(\n        realSend,\n        function getRequestUrl() {\n            return this._requestUrl;\n        },\n        extractBody,\n        \'xmlhttprequest\'\n    );\n\n    /*\n     * This function is intercepted so that we can capture the value of the\n     * request URL, since it isn\'t present in the arguments for\n     * XMLHttpRequest.send (above).\n     */\n    window.XMLHttpRequest.prototype.open = function open(...args) {\n        [, this._requestUrl] = args;\n        realOpen.apply(this, args);\n    };\n    window.XMLHttpRequest.prototype.open.toString = () => realOpen.toString();\n\n    /*\n     * Monkeypatches for setting the src of images and scripts, an\n     * indirect way of sending requests that is often employed by\n     * skimmers. The getters are also overridden to make the behaviour\n     * seem identical to the original implementation. Otherwise, if a\n     * script were to set an src and immediately read it, it would\n     * come back as the old value.\n     */\n\n    /*\n     * WeakMap maps an object to a value. This is used to store the\n     * new src of an object when its true src property may not yet\n     * have been set.\n     */\n    const srcMap = new WeakMap();\n    const requestElements = [\n        {\n            srcElement: HTMLImageElement.prototype,\n            type: \'image\',\n        },\n        {\n            srcElement: HTMLScriptElement.prototype,\n            type: \'script\',\n        },\n    ];\n\n    for (const { srcElement, type } of requestElements) {\n        const realGetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\')\n            .get;\n        const realSetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\')\n            .set;\n        const realGetAttribute = srcElement.getAttribute;\n        const realSetAttribute = srcElement.setAttribute;\n        const realRemoveAttribute = srcElement.removeAttribute;\n\n        const interceptFn = generateRequestInterceptFn(\n            realSetSrc,\n            src => `${src}`,\n            null,\n            type\n        );\n\n        function setSrc(url) {\n            /*\n             * It\'s valid to provide a TrustedScriptURL wrapping the URL but we need to\n             * return a string in the getter, so we convert the url to a string here.\n             *\n             * The url can also be undefined/null so we can\'t just call `.toString()`.\n             *\n             * See #56.\n             */\n            srcMap.set(this, `${url}`);\n            interceptFn.call(this, url);\n            return url;\n        }\n\n        function getSrc() {\n            if (srcMap.get(this) === undefined) {\n                return realGetSrc.call(this);\n            } else {\n                return normaliseUrl(srcMap.get(this));\n            }\n        }\n\n        getSrc.toString = () => realGetSrc.toString();\n        setSrc.toString = () => realSetSrc.toString();\n\n        Object.defineProperty(srcElement, \'src\', {\n            get: getSrc,\n            set: setSrc,\n        });\n\n        srcElement.setAttribute = function(attributeName, value) {\n            if (attributeName.toUpperCase() === \'SRC\') {\n                setSrc.call(this, value);\n            } else {\n                realSetAttribute.call(this, attributeName, value);\n            }\n        };\n        srcElement.setAttribute.toString = () => realSetAttribute.toString();\n\n        srcElement.getAttribute = function(attributeName) {\n            if (\n                attributeName.toUpperCase() === \'SRC\' &&\n                srcMap.get(this) !== undefined\n            ) {\n                return srcMap.get(this);\n            } else {\n                return realGetAttribute.call(this, attributeName);\n            }\n        };\n        srcElement.getAttribute.toString = () => realGetAttribute.toString();\n\n        srcElement.removeAttribute = function(attributeName) {\n            if (attributeName.toUpperCase() === \'SRC\') {\n                srcMap.delete(this);\n            }\n\n            return realRemoveAttribute.call(this, attributeName);\n        };\n        srcElement.removeAttribute.toString = () =>\n            realRemoveAttribute.toString();\n    }\n})(\n    window.XMLHttpRequest.prototype.open,\n    window.XMLHttpRequest.prototype.send,\n    window.fetch\n);\n</script><script>/*\n * This entire block is wrapped in an IIFE to prevent polluting the scope of the web page with\n * functions created by this extension.\n */\n(function(realOpen, realSend, realFetch) {\n    /*\n     * Sometimes pages overwrite standard objects with global\n     * variables. Where possible, we extract fresh versions from an\n     * iframe.\n     */\n    const iframe = document.createElement(\'iframe\');\n    document.documentElement.appendChild(iframe);\n\n    // Initialise with global values\n    let URL = window.URL;\n    let WeakMap = window.WeakMap;\n    let WeakSet = window.WeakSet;\n    let TextDecoder = window.TextDecoder;\n    let Uint8Array = window.Uint8Array;\n    let decodeURIComponent = window.decodeURIComponent;\n    let URLSearchParams = window.URLSearchParams;\n    let Array = window.Array;\n    let Object = window.Object;\n    let atob = window.atob;\n    let CustomEvent = window.CustomEvent;\n    let performance = window.performance;\n    let JSON = window.JSON;\n\n    /*\n     * Under some circumstances the iframe isn\'t added\n     * successfully. If it has a contentWindow then we can extract the\n     * values from it, but otherwise we fall back on the global\n     * values.\n     */\n    if (iframe.contentWindow) {\n        // Try block because sometimes the CORS prevents access to iframe\n        try {\n            URL = iframe.contentWindow.URL;\n            WeakMap = iframe.contentWindow.WeakMap;\n            WeakSet = iframe.contentWindow.WeakSet;\n            TextDecoder = iframe.contentWindow.TextDecoder;\n            Uint8Array = iframe.contentWindow.Uint8Array;\n            decodeURIComponent = iframe.contentWindow.decodeURIComponent;\n            URLSearchParams = iframe.contentWindow.URLSearchParams;\n            Array = iframe.contentWindow.Array;\n            Object = iframe.contentWindow.Object;\n            atob = iframe.contentWindow.atob;\n            CustomEvent = iframe.contentWindow.CustomEvent;\n            performance = iframe.contentWindow.performance;\n            JSON = iframe.contentWindow.JSON;\n        } catch (error) {\n            // Empty catch block to prevent errors from showing in page logs\n        }\n    }\n\n    // If iframe was added successfully, remove it\n    if (iframe.parentNode) {\n        iframe.parentNode.removeChild(iframe);\n    }\n\n    /*\n     * If any of the values don\'t have the correct type, return and\n     * don\'t attempt any monkey patching. This avoids breaking web\n     * pages.\n     */\n    for (f of [\n        URL,\n        WeakMap,\n        WeakSet,\n        TextDecoder,\n        Uint8Array,\n        decodeURIComponent,\n        URLSearchParams,\n        Array,\n        Object,\n        atob,\n        CustomEvent,\n    ]) {\n        if (typeof f !== \'function\') {\n            return;\n        }\n    }\n\n    for (o of [performance, JSON]) {\n        if (typeof o !== \'object\') {\n            return;\n        }\n    }\n\n    /*\n     * Override common funcions that accept callbacks, such as\n     * setInterval, to keep track of what script each callback comes\n     * from. Skimmers commonly have the actual skimming code in a\n     * callback, so most of the time when we intercept a request,\n     * document.currentScript is null. To solve this, the script is\n     * stored in a closure when the callback is passed, and assigned\n     * to callbackCurrentscript when the callback is run.\n     */\n\n    let callbackCurrentScript = null;\n\n    // Maps callbacks originally passed to patched versions\n    const patchedCallbackMap = new WeakMap();\n\n    // Maps patched callbacks to original versions\n    const originalCallbackMap = new WeakMap();\n\n    /**\n     *  Generates the function with which to override the\n     *  callback-accepting functions.\n     *  @param realFn The real function, e.g. setInterval.\n     *  @param callbackIndex The index of the callback in the\n     *  arguments of realFn. For example, the callback is the first\n     *  argument in setInterval, so callbackIndex should be 0.\n     */\n    function generateAddCallbackFunction(realFn, callbackIndex) {\n        const patched = function(...args) {\n            const script = document.currentScript || callbackCurrentScript;\n\n            const callback = args[callbackIndex];\n\n            // If the callback isn\'t a function, there\'s nothing to patch\n            if (typeof callback !== \'function\') {\n                return realFn.apply(this, args);\n            }\n\n            // If a patch exists, use it instead of creating new function\n            let patchedCallback = patchedCallbackMap.get(callback);\n            if (!patchedCallback) {\n                patchedCallback = function() {\n                    callbackCurrentScript = script;\n                    const retVal = callback.apply(this, [...arguments]);\n                    callbackCurrentScript = null;\n                    return retVal;\n                };\n                patchedCallbackMap.set(callback, patchedCallback);\n                originalCallbackMap.set(patchedCallback, callback);\n            }\n\n            args[callbackIndex] = patchedCallback;\n            return realFn.apply(this, args);\n        };\n        // make toString() look native, #34\n        patched.toString = () => realFn.toString();\n        return patched;\n    }\n\n    const realSetInterval = window.setInterval;\n\n    window.setInterval = generateAddCallbackFunction(realSetInterval, 0);\n\n    const realSetTimeout = window.setTimeout;\n\n    window.setTimeout = generateAddCallbackFunction(realSetTimeout, 0);\n\n    const realPromiseThen = Promise.prototype.then;\n    Promise.prototype.then = generateAddCallbackFunction(realPromiseThen, 0);\n\n    const realPromiseCatch = Promise.prototype.catch;\n    Promise.prototype.catch = generateAddCallbackFunction(realPromiseCatch, 0);\n\n    const realPromiseFinally = Promise.prototype.finally;\n    if (realPromiseFinally) {\n        Promise.prototype.finally = generateAddCallbackFunction(\n            realPromiseFinally,\n            0\n        );\n    }\n\n    const realAddEventListener = EventTarget.prototype.addEventListener;\n\n    EventTarget.prototype.addEventListener = generateAddCallbackFunction(\n        realAddEventListener,\n        1\n    );\n\n    /*\n     * removeEventListener needs to be patched to map the passed\n     * listener to the patched listener.\n     */\n    const realRemoveEventListener = EventTarget.prototype.removeEventListener;\n    EventTarget.prototype.removeEventListener = function(...args) {\n        args[1] = patchedCallbackMap.get(args[1]) || args[1];\n\n        return realRemoveEventListener.apply(this, args);\n    };\n    EventTarget.prototype.removeEventListener.toString = () =>\n        realRemoveEventListener.toString();\n\n    /*\n     * Event listeners are sometimes set by directly assigning to a\n     * property from the GlobalEventHandlers mixin,\n     * e.g. \'submitBtn.onclick\'. For each of the GlobalEventHandlers\n     * implementations that are likely to be relevant, the event\n     * properties likely to be used by skimmers are overridden with\n     * our version that tracks currentScript when assigned.\n     */\n    for (const eventTarget of [\n        HTMLElement.prototype,\n        Document.prototype,\n        window,\n    ]) {\n        for (const eventName of [\n            \'onclick\',\n            \'onblur\',\n            \'onfocus\',\n            \'onchange\',\n            \'oninput\',\n            \'onkeydown\',\n            \'onkeypress\',\n            \'onkeyup\',\n            \'onload\',\n            \'onloadend\',\n            \'onmousedown\',\n            \'onmouseenter\',\n            \'onmouseleave\',\n            \'onmousemove\',\n            \'onmouseout\',\n            \'onmouseover\',\n            \'onmouseup\',\n            \'onpointerdown\',\n            \'onpointermove\',\n            \'onpointerup\',\n            \'onpointerover\',\n            \'onpointerout\',\n            \'onpointerenter\',\n            \'onpointerleave\',\n            \'onsubmit\',\n        ]) {\n            const descriptor = Object.getOwnPropertyDescriptor(\n                eventTarget,\n                eventName\n            );\n\n            if (!descriptor) {\n                continue;\n            }\n\n            const realGetter = descriptor.get;\n            const realSetter = descriptor.set;\n\n            const patchedSetter = generateAddCallbackFunction(realSetter, 0);\n\n            /*\n             * In case a script checks the value of the property after\n             * setting it, the getter has to be patched to return the\n             * original callback.\n             */\n            const patchedGetter = function() {\n                const realValue = realGetter.call(this);\n\n                const originalCallback = originalCallbackMap.get(realValue);\n                if (originalCallback) {\n                    return originalCallback;\n                } else {\n                    return realValue;\n                }\n            };\n\n            patchedGetter.toString = () => realGetter.toString();\n\n            Object.defineProperty(eventTarget, eventName, {\n                get: patchedGetter,\n                set: patchedSetter,\n            });\n        }\n    }\n\n    /*\n     * This script has some dependencies that need to be included\n     * without polluting the global namespace. This is achieved by\n     * inserting them into the source at build time in the\n     * makefile. This is preferable to loading the dependencies via\n     * AJAX, because the latter doesn\'t work if the CSP disallows\n     * unsafe-eval. The following tag tells the makefile where to\n     * insert the dependencies.\n     */\n    // <DEPENDENCIES>\nclass InputAnalyser {\n    constructor(_inputs) {\n        this._inputs = _inputs;\n        this._irrelevantInputs = new WeakSet();\n\n        /*\n         * Not currently using Password because it was causing too\n         * many false positives. Some sites use external auth domains,\n         * and many have misconfigured analytics that collect\n         * passwords.\n         */\n        this.relevantInputs = [new CardNumber()];\n    }\n\n    get relevantInputValues() {\n        const inputs = {};\n        this._inputs.forEach(\n            input => (inputs[input.id || input.name] = input.value)\n        );\n\n        return inputs;\n    }\n\n    get relevantFilledInputs() {\n        return this.allInputs\n            .filter(input => this.isRelevantElement(input))\n            .filter(input => input.value.length >= cons.minInputSize)\n            .map(input => ({\n                id: input.id,\n                name: input.name,\n                value: input.value,\n            }));\n    }\n\n    get allInputs() {\n        this._inputs = [\n            ...document.querySelectorAll(\n                this.relevantInputs.map(i => i.relevantInputTypes).join(\', \')\n            ),\n        ];\n\n        return this._inputs;\n    }\n\n    static getLabelText(input) {\n        if (!input.labels.length) {\n            return \'\';\n        }\n        return stripSpecialChars(input.labels[0].textContent).trim();\n    }\n\n    /**\n     * Takes an HTMLElement and determines whether or not it could be relevant for the purposes of\n     * skimmer detection.\n     * @param {HTMLElement} input The element which is to be checked for relevancy.\n     * @returns {boolean} Returns true if the element is potentially relevant (e.g. a credit card\n     * field or other such sensitive input).\n     */\n    isRelevantElement(input) {\n        // If the element has been identified as irrelevant, return false\n        if (this._irrelevantInputs.has(input)) {\n            return false;\n        }\n\n        // Check the input against all the relevant input types\n        const relevances = this.relevantInputs.map(i => i.test(input));\n\n        /*\n         *  If they all agree that this input is unlikely to become\n         *  relevant, exclude it from further checks and return false\n         */\n        if (relevances.every(i => i === RelevantInput.relevance.NOT_RELEVANT)) {\n            this._irrelevantInputs.add(input);\n            return false;\n        }\n\n        // Return true if any consider the input to be relevant, otherwise false\n        return relevances.some(i => i === RelevantInput.relevance.RELEVANT);\n    }\n}\n/*\n * Input Matcher represents a class that can have matches with relevant inputs.\n * Each class that inherits must implement the findInputMatches method.\n */\nclass InputMatcher {\n    /*\n     * Finds any matches with the list of inputs.\n     * @param inputs. An object whose values represent relevant inputs.\n     * @return {[String]} The keys of the inputs that were found.\n     */\n    findInputMatches(inputs) {\n        throw new Error(\'findInputMatches has not been implemented.\');\n    }\n\n    /**\n     * Determines whether one or more values in needles are present in any of\n     * the keys of the haystacks.\n     * Checks both raw input and base64 encoded versions of the needles.\n     * @param needles An object. The haystacks are searched for the values of\n     * this object.\n     * @param haystacks The array of strings in which the needle(s) may reside.\n     * A string may optionally be supplied if only one value is present.\n     * @returns {[String]} The keys of the needles that were found in one or\n     * more haystacks in either raw or base64 encoded form.\n     */\n    static atLeastOneNeedleInHaystack(needles, haystacks) {\n        if (\n            needles === undefined ||\n            needles.length === 0 ||\n            haystacks === undefined\n        ) {\n            return [];\n        }\n\n        const searchArray = Array.isArray(haystacks) ? haystacks : [haystacks];\n\n        return Object.entries(needles)\n            .filter(([_, needle]) =>\n                searchArray.some(haystack => {\n                    if (isBase64Encoded(haystack)) {\n                        if (atob(haystack).indexOf(needle) !== -1) {\n                            return true;\n                        }\n                    }\n                    return haystack.indexOf(needle) !== -1;\n                })\n            )\n            .map(([input, _]) => input);\n    }\n}\n/*\n * Matcher for an object that cannot contain any inputs.\n */\nclass EmptyMatcher extends InputMatcher {\n    findInputMatches(inputs) {\n        return [];\n    }\n}\n\nclass URLParamsMatcher extends InputMatcher {\n    constructor(url) {\n        super();\n        this._url = url;\n    }\n\n    findInputMatches(inputs) {\n        try {\n            const parsedUrl = new URL(this._url);\n            return InputMatcher.atLeastOneNeedleInHaystack(inputs, [\n                ...parsedUrl.searchParams.values(),\n            ]);\n        } catch (e) {\n            return [];\n        }\n    }\n}\n\nclass StringBodyMatcher extends InputMatcher {\n    constructor(str) {\n        super();\n        this._str = str;\n    }\n\n    findInputMatches(inputs) {\n        let decoded;\n        try {\n            decoded = decodeURIComponent(this._str);\n        } catch (e) {\n            decoded = this._str;\n        }\n\n        const searchParams = new URLSearchParams(decoded);\n        const parsedQuery = {};\n        const parsedValues = [];\n\n        for (const [key, value] of searchParams.entries()) {\n            parsedQuery[key] = value;\n            parsedValues.push(value);\n        }\n\n        if (parsedQuery !== {} && parsedValues.indexOf(null) === -1) {\n            return InputMatcher.atLeastOneNeedleInHaystack(\n                inputs,\n                parsedValues\n            );\n        }\n        return InputMatcher.atLeastOneNeedleInHaystack(inputs, decoded);\n    }\n}\n\nclass FormBodyMatcher extends InputMatcher {\n    constructor(formData) {\n        super();\n        this._formData = formData;\n    }\n\n    findInputMatches(inputs) {\n        return InputMatcher.atLeastOneNeedleInHaystack(\n            inputs,\n            Object.values(this._formData).flat()\n        );\n    }\n}\n/**\n * Takes a string and removes a variety of \'special\' characters like punctuation.\n * @param str The string to be modified.\n * @returns {string} The resulting string without any special characters.\n */\nfunction stripSpecialChars(str) {\n    return str.replace(/[`~!@#$%^&*()_|+\\-=?;:\'\",.<>}{\\][\\\\/]/gi, \'\');\n}\n\n/**\n * Attempts to identify where a string is base-64 encoded or not.\n * @param string The raw string to be examined.\n * @returns {boolean} Returns true if the string matches a base-64 string. False otherwise.\n */\nfunction isBase64Encoded(string) {\n    return /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/.test(\n        string\n    );\n}\n\n// Computes the base64 sha512/sha224 hash of some text, removing trailing ==\nfunction base64hash(hashFunction, text) {\n    return btoa(\n        hashFunction\n            .array(text)\n            .map(char => String.fromCharCode(char))\n            .join(\'\')\n    ).slice(0, -2);\n}\n\nfunction normaliseUrl(str) {\n    const link = document.createElement(\'a\');\n    link.href = str;\n    return link.href;\n}\n\nfunction stripCredentials(urlstr) {\n    try {\n        const url = new URL(urlstr);\n        const params = url.searchParams;\n        params.forEach((_, key) => params.set(key, \'\'));\n        return url.toString();\n    } catch (e) {\n        return urlstr;\n    }\n}\n\n/**\n * Check if two URLs match regardless of any partial encoding.\n *\n * Uses decodeURI which can throw a URIError when url is malformed.\n */\nfunction urlsMatch(first, second) {\n    try {\n        return decodeURI(first) === decodeURI(second);\n    } catch (e) {\n        return false;\n    }\n}\n\nfunction sendMessage(type, data, tabId, options) {\n    if (tabId) {\n        chrome.tabs.sendMessage(tabId, { data, type }, options);\n    } else {\n        chrome.runtime.sendMessage({ data, type });\n    }\n}\n\n// Encodes the given input using base64url as defined by RFC4648 section 5.\nfunction base64Url(input) {\n    return btoa(input)\n        .replace(\'+\', \'-\')\n        .replace(\'/\', \'_\');\n}\n/*\n * Netcraft Extension\n * constants JavaScript\n */\n\n/* eslint sort-keys: \'off\' */\n/* sort-keys is disabled as the constants are separated by type */\n\n// Aligned values here make for easier reading, so we disable prettier\n\n// prettier-ignore\nconst cons = (function() {\n    const cryptojackersUrl = \'https://www.netcraft.com/apps/help/glossary/#crypto-miners\';\n    const skimmersUrl = \'https://www.netcraft.com/apps/help/glossary/#credit-card-skimmers\';\n    const evilResourceCrime = \'attempted to load\';\n\n    return {\n        fields: [\n            \'topsites\',\n            \'country\',\n            \'netblock\',\n            \'hoster\',\n            \'firstseen\',\n            \'rank\',\n            \'risk\',\n            \'uses_sslv3\',\n            \'pfs\',\n            \'heartbleed\',\n            \'heartbleed_message\',\n        ],\n\n        // options that can be toggle in options.js and installed flag\n\n        toggleOptions: [\n            \'installed\',\n            \'block\',\n            \'xss\',\n            \'suspicious\',\n            \'other-malicious-scripts\',\n            \'credential-leaks\',\n            \'analytics\',\n            \'cryptojackers\',\n            \'skimmers\',\n        ],\n\n        // domains\n        mirror:  \'https://mirror2.extension.netcraft.com/\',\n        toolbar: \'https://mirror.toolbar.netcraft.com/\',\n\n        // links and faqs\n        blogArchive:      \'https://news.netcraft.com/archives/\',\n        browserApp:       \'https://www.netcraft.com/apps/browser/\',\n        cryptojackersUrl,\n        extensionStore:   \'https://addons.mozilla.org/en-us/firefox/addon/netcraft-toolbar?src=external-blocked_visit-rating\',\n        heartbleedUrl:    \'https://www.netcraft.com/apps/help/glossary/#heartbleed\',\n        logBlockedVisit:  \'https://toolbar.netcraft.com/blocked_visit\',\n        netblock:         \'https://sitereport.netcraft.com/netblock?q=\',\n        pfsUrl:           \'https://www.netcraft.com/apps/help/glossary/#pfs\',\n        report:           \'https://sitereport.netcraft.com/?url=\',\n        reportAPI:        \'https://report.netcraft.com/api/v2/report/urls\',\n        reportMistake:    \'https://report.netcraft.com/report/mistake?url=\',\n        skimmersUrl,\n        sslv3Url:         \'https://www.netcraft.com/apps/help/glossary/#sslv3\',\n        submissionURL:    \'https://report.netcraft.com/submission/\',\n        topsites:         \'https://trends.netcraft.com/topsites?s=\',\n\n        // paths\n        block:              \'blocked.html\',\n        check:              \'check_url/v4/\',\n        danger:             \'Images/danger.png\',\n        embed:              \'/embedded\',\n        evilJSPatternsFeed: \'eviljspatterns\',\n        skimmerDomainWhitelistFeed: \'eviljsskimmerwhitelist\',\n        evilResourcesFeed:  \'blockdb/netcraft_extension_evil_resource_encrypted-yppL7bVh/\',\n        flags:              \'Images/flags/\',\n        force:              \'/dodns\',\n        heartbleed:         \'Images/heartbleed.png\',\n        info:               \'/info\',\n        poodlePost:         \'2014/10/15/googles-poodle-affects-oodles.html\',\n        warning:            \'Images/warning.png\',\n\n        // feed constants\n        evilResourceTypes: [\'cryptojacker\', \'skimmer\'],\n        evilResourcesSalt: \'C4jhYHxW\',\n        maxRevisions:      5,\n        randomSaltLength:  8,\n\n        // mappings of url_type => information\n        urlTypeInformationMappings: {\n            \'malicious_javascript/cryptojacker\': [\'web miners\',             cryptojackersUrl],\n            \'malicious_javascript/skimmer\':      [\'shopping site skimmers\', skimmersUrl],\n        },\n\n        // mappings of evilResourceTypes => user options\n        evilResourceOptionMappings: {\n            \'cryptojacker\': \'cryptojackers\',\n            \'skimmer\':      \'skimmers\'\n        },\n\n        // mappings of user options => url_type\n        optionUrlTypeMappings: {\n            \'block\':                   /^(?!malicious_javascript:)/,\n            \'cryptojackers\':           /^malicious_javascript\\/cryptojacker$/,\n            \'other-malicious-scripts\': /^malicious_javascript\\/(?!(skimmer|cryptojacker)$)/,\n            \'skimmers\':                /^malicious_javascript\\/skimmer$/,\n        },\n\n        // report constants\n        malwareSourceEmail:  \'extension-reported-malware@netcraft.com\',\n        malwareSourceUUID:   \'mwcCcApi89cobY9loJ9rWJ9HuOhlpwMj\',\n        extensionSourceUUID: \'qBEQ3NhEnwK4sypmuSxBJzRqpxZLRXxe\',\n\n        // report error default message\n        reportErrorMessage: \'An error occured with your submission. Please try again.\',\n\n        // mapping from report API errors to displayed messages\n        reportErrorMappings: {\n            \'Duplicate of a recent submission\': \'This URL has already been reported.\',\n            \'Supplied email is not valid\': \'Invalid email address. Please correct it and try again.\',\n        },\n\n        whitelistErrorMessages: {\n            invalidHostname: \'Invalid hostname, please correct it and try again\',\n            duplicate: \'Protection is already disabled for this site\',\n        },\n\n        // browserAction icon paths\n        iconNormal: {\n            16: \'Images/action-normal-16.png\',\n            24: \'Images/action-normal-24.png\',\n            32: \'Images/action-normal-32.png\'\n        },\n        iconDanger: {\n            16: \'Images/action-danger-16.png\',\n            24: \'Images/action-danger-24.png\',\n            32: \'Images/action-danger-32.png\'\n        },\n        iconWarning: {\n            16: \'Images/action-warning-16.png\',\n            24: \'Images/action-warning-24.png\',\n            32: \'Images/action-warning-32.png\'\n        },\n        iconDisabled: {\n            16: \'Images/action-disabled-16.png\',\n            24: \'Images/action-disabled-24.png\',\n            32: \'Images/action-disabled-32.png\'\n        },\n\n        // Extension internal message types\n        capturedIntercept:  \'interceptDetailsCaptured\',\n        confirmedIntercept: \'interceptDetailsConfirmed\',\n        contentReady:       \'contentScriptReady\',\n        leaksEnabled:       \'credentialLeaksEnabled\',\n        reportURL:          \'report\',\n        reqBlock:           \'block\',\n        reqDetails:         \'details\',\n        reqForce:           \'force\',\n        reqPopUp:           \'popup\',\n        reqPrintTimingData: \'time\', //debug\n        reqInputs:          \'requestInputValues\',\n        sendInputs:         \'sendInputs\',\n        sendIntercept:      \'sendInterceptDetails\',\n        updateOption:       \'updateOption\',\n        updateWhitelist:    \'whitelist\', // disable button whitelist\n\n        // Status codes\n        errEvilResource: \'EVILRESOURCE\',\n        errNoDetails:    \'NODETAILS\',\n        errNoInfo:       \'NOINFO\',\n        errBlocked:      \'BLOCKED\',\n        errPending:      \'PENDING\',\n        errServer:       \'ERROR\',\n        errSuspicious:   \'SUSPICIOUS\',\n        errTimeout:      \'TIMEOUT\',\n        errXSS:          \'XSS\',\n        noError:         \'OK\',\n\n        // Valid protocols (it is a regex match so ^ matches start of string)\n        protocols:    [\'^http\', \'^https\', \'^ftp\'],\n\n        // Human readable messages\n        msgNa:           \'NA\',\n        msgNewSite:      \'New Site\',\n        msgNoInfo:       \'Site information not available\',\n        msgNoServer:     \'Unable to contact Netcraft servers\',\n        msgSuspicious:   \'Suspicious URL Detected\',\n        msgXSS:          \'Suspected XSS Attack\',\n        msgBlocked:      {\n            \'malicious_javascript/skimmer\': {\n                title: \'Shopping Site Skimmer Detected\',\n                description: \'shopping site skimmer\'\n            },\n            \'malicious_javascript/dropsite\': {\n                title: \'Potential Credential Leak Detected\',\n                description: \'potential credential leak\'\n            },\n            \'malicious_javascript/cryptojacker\': {\n                title: \'Web Miner Detected\',\n                description: \'web miner\'\n            },\n            \'malware_c2/infrastructure\': {\n                title: \'Malicious URL\',\n                description: \'malicious page\'\n            },\n            \'default\': {\n                title: \'Suspected Phishing\',\n                description: \'suspected phishing site\'\n            },\n        },\n\n        // Human readable reasons\n        urlTypeCrimeMappings: {\n            \'malicious_javascript/skimmer\':      evilResourceCrime,\n            \'malicious_javascript/cryptojacker\': evilResourceCrime,\n            \'malicious_javascript/dropsite\':     \' is blocked because we detected that sensitive information was leaked from\',\n        },\n\n        // Timeout function value to clear cache etc.\n        timeout:      300000,\n\n        // Skimmer whitelist update interval (1 day)\n        skimmerWhitelistInterval: 1000 * 60 * 60 * 24,\n\n        // Google Analytics account\n        account:      \'UA-2150242-8\',\n        gaCategory:   \'Popup load times\',\n\n        // InputAnalyser constants\n        minInputSize: 5,\n\n        // First addresses of the private IPv4 netblocks\n        privateIPv4Blocks: [\n            \'10.0.0.0\',\n            \'172.16.0.0\',\n            \'192.168.0.0\'\n        ],\n\n        // Sync storage key for disable for this site button whitelist\n        disableButtonWhitelistKey: \'disableButtonWhitelist\',\n\n        // Local storage key for local credential leak detection whitelist\n        localCredentialLeakWhitelistKey: \'localCredentialLeakWhitelist\',\n\n        // Whitelisted fileName regexes for credential leak detection\n        credentialLeakFileNameWhitelist: [\n            /^https?:\\/\\/googleads.g.doubleclick.net\\//,\n        ],\n    };\n})();\n\n/*\n * recursively prevents any property or sub-property in cons from:\n *   - being added;\n *   - being removed; or\n *   - being modified.\n */\nfunction deepFreeze(object) {\n    Object.freeze(object);\n\n    for (const name of Object.getOwnPropertyNames(object)) {\n        if (typeof object[name] === \'object\') {\n            deepFreeze(object[name]);\n        }\n    }\n}\n\ndeepFreeze(cons);\n/*\n * RelevantInput objects check whether given form inputs should be\n * treated as sensitive data.\n *\n * This should be treated as an abstract class. Only the subclasses\n * are intended to be instantiated.\n */\nclass RelevantInput {\n    /*\n     * Enum of the possible return values of test():\n     * RELEVANT:\n     *     Input is relevant now, outgoing requests should be\n     *     checked for the contained value (subject to minimum length).\n     * POTENTIALLY RELEVANT:\n     *     Input should not be considered relevant now, but may be in\n     *     future so should continue to be checked.\n     * NOT_RELEVANT:\n     *     Input is not relevant and will not become relevant, so can\n     *     be ommitted from further checks to optimise performance.\n     */\n    static get relevance() {\n        return {\n            NOT_RELEVANT: 1,\n            POTENTIALLY_RELEVANT: 2,\n            RELEVANT: 3,\n        };\n    }\n\n    /*\n     * relevantInputTypes (string) - a comma separated list of\n     * selectors that describe the types of input that should be\n     * considered relevant.\n     */\n    constructor(relevantInputTypes) {\n        this.relevantInputTypes = relevantInputTypes;\n    }\n\n    /*\n     * Checks whether any of the given input\'s \'labels\' (the ID, name,\n     * or actual label) match the given regex.\n     */\n    static testInputLabel(regex, input) {\n        const labels = [\n            input.id,\n            input.name,\n            InputAnalyser.getLabelText(input),\n        ];\n        return labels.some(label => regex.test(label));\n    }\n}\n\n/*\n * RelevantInput for passwords.\n */\nclass Password extends RelevantInput {\n    constructor() {\n        super(\'input[type=password]\');\n    }\n\n    /*\n     * Passwords are verified purely by their input type.\n     */\n    test(input) {\n        if (input.matches(this.relevantInputTypes)) {\n            return RelevantInput.relevance.RELEVANT;\n        } else {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n    }\n}\n\n/*\n * RelevantInput for card numbers.\n */\nclass CardNumber extends RelevantInput {\n    constructor() {\n        // Inputs with no type attribute act like text inputs\n        super(\n            \'input[type=text], input[type=tel], input[type=number], input:not([type])\'\n        );\n\n        // Definitely card number labels\n        this.inputLabelRegexes = [\n            /(cc|card).?(num|no)/i,\n            /&#12459;&#12540;&#12489;&#30058;&#21495;/,\n            /&#1053;&#1086;&#1084;&#1077;&#1088;.*&#1082;&#1072;&#1088;&#1090;&#1099;/i,\n            /&#20449;&#29992;&#21345;&#21345;&#34399;/i,\n            /&#20449;&#29992;&#21345;&#21495;&#30721;/i,\n        ];\n\n        /*\n         * Matches incomplete card numbers, because ideally the\n         * skimmer should be detected before the victim gives away\n         * most of their card number.\n         */\n        this.inputValueRegexes = [/^(\\d{4}[-\\s]?){0,4}\\d{1,4}$/];\n\n        // Probably card number labels\n        this.suspectInputLabelRegexes = [\n            /card/i,\n            /tarjeta/i,\n            /carte/i,\n            /&#1082;&#1072;&#1088;&#1090;&#1099;/i,\n            /&#21345;/i,\n            /&#12459;&#12540;&#12489;/i,\n            /&#52852;&#46300;/i,\n            /credit[^s]/i,\n        ];\n\n        // If it matches one of these, probably not a card number\n        this.inputLabelExceptionRegexes = [\n            /post/i,\n            /code/i,\n            /zip/i,\n            /acc(oun)?t?[-_]?n(um|o)/i,\n            /credit[-_]?(sum|limit)/i,\n            /loyalty/i,\n            /gift/i,\n            /idcard/i,\n            /cardid/i,\n            /club/i,\n            /sku/i,\n            /(\\b|_)otp/i,\n            /price/i,\n            /amount/i,\n            /service/i,\n            /picard/i,\n            /search/i,\n            /contact/i,\n            /callme/i,\n            /chat/i,\n            /activate/i,\n            /msisdn/i,\n        ];\n    }\n\n    test(input) {\n        if (!input.matches(this.relevantInputTypes)) {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n\n        // Return NOT_RELEVANT if label matches an exception regex\n        if (\n            this.inputLabelExceptionRegexes.some(r =>\n                RelevantInput.testInputLabel(r, input)\n            )\n        ) {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n\n        // Return RELEVANT if label clearly states card number\n        if (\n            this.inputLabelRegexes.some(r =>\n                RelevantInput.testInputLabel(r, input)\n            )\n        ) {\n            return RelevantInput.relevance.RELEVANT;\n        }\n\n        /*\n         * If label suggests card number, return RELEVANT if value\n         * also suggests card number or POTENTIALLY_RELEVANT if\n         * not. Otherwise, return NOT_RELEVANT as this element will\n         * not become relevant unless a label changes (unlikely).\n         */\n        if (\n            this.suspectInputLabelRegexes.some(r =>\n                RelevantInput.testInputLabel(r, input)\n            )\n        ) {\n            if (this.inputValueRegexes.some(r => r.test(input.value))) {\n                return RelevantInput.relevance.RELEVANT;\n            } else {\n                return RelevantInput.relevance.POTENTIALLY_RELEVANT;\n            }\n        } else {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n    }\n}\n\n    const inputAnalyser = new InputAnalyser();\n\n    const resourceBlockList = new Set([\n                            \"6C590F97DDB1A11AE795B3050E4C3866\", \"D285BE79786E2ECE396941331C5BA65E\", \"59DF8523520DE7F08FB53D3D12BD94F5\", \"1FB784EBAFDA1618023756BF5B6D7E46\", \"0D6E1B5F03B81F5C37813B22E29C4FE2\", \"EFAB0ECE9383D50C555B8168C3E2A4DD\", \"AC8A4B13A411A4E43E61D0A034FA4A55\", \"25A781F6D1F94EEACCF1E00E844D9E14\", \"3920B04ED0D215EA738F524F60485F23\", \"05F4A2D3161A92EB82A44F15FC2A3AEC\", \"C5A1BE7B3855B0A25275B2877E005CB9\", \"00069CEC347076721EBE2257978A0A33\", \"9A513130D02F360BBDC16D8AC0C579D8\", \"7CDB2A3A29E50F90C1000252EF523AD6\", \"25CE912FAF2376026AAD0174AD5A0329\", \"739E1F67AA5F64A06FC469F6030802D4\", \"F49368C9A05C29EF49411999C5987F2C\", \"AB2DD89FB892155275D8780CF527BC93\", \"CFE758EB18FBFE70B1F690241B75D2B1\", \"E43C3463B40A790545F4149FF06B419E\", \"8B6D4C76A7CCF5EB720660B8B2D3F5EB\", \"45573B3E2F613AF5BF2145DF004BABD1\", \"E3DB4E968B371DE94CE607199A062F39\", \"A4EFF1240C5DE614B394FB738CBD28E2\", \"F42D10E84B9A69A8A24062319F425A83\", \"90646BE39982594A58EDF5D2630CCFFD\", \"669853BEA9B84EA51D7302A7FBFF5BC3\", \"0BA566B4777526F451108100107DC5ED\", \"AB19E12BFFBC47244BDCC2403EAA9037\", \"61DAF8537240639277FD1A5908A131A4\", \"6F1DC1309CFE734C038685D8F17A3B6B\", \"DCC3DB001334F29C1C0208D15E9EAB49\", \"2DBD7D54022C28273ADE3147B8EEB076\", \"AE3760E93412C65EC5BF63537B037019\", \"4CD96A830DAC0E0B5EBE578B0DD870AE\", \"4C70569BBE1821F5655382660807FA77\", \"05ECA17FF0EA78669ED43B2BD7B63571\", \"44E503B3621513B9D3666F284E4DF47A\", \"8F1C92E7665860AE70CBB228DED86111\", \"D69552885B55C4097E5725F805815639\", \"B4357A9D889E974CDF3F9740E48F9A1B\", \"5709DC60CFC365B7064B06C709A9D32B\", \"14E8729CBB16AC3625EAA0D7A679E56E\", \"793D4DABAAA377A12F2B6CE88C5FB389\", \"C5D1DCE2AE0D8CE79054FC16CB13D65A\", \"89F91750BD4A9397D1F8CE183757FF70\", \"50B8BC213FB188A687173C1A0A77EA57\"\n                        ]);\n    const evilJSPatterns = new Set([\n                            \n                        ]);\n\n    // Extract body object\n    function extractBody(data) {\n        if (!data) {\n            return Promise.resolve({});\n        }\n        if (FormData.prototype.isPrototypeOf(data)) {\n            const formData = {};\n            for (const [key, value] of data.entries()) {\n                formData[key] = value;\n            }\n            return Promise.resolve({ formData });\n        }\n        // Create a temporary Request object to parse the body\n        const tempRequest = new Request(\'\', { body: data, method: \'POST\' });\n        return extractBodyFromRequest(tempRequest);\n    }\n\n    function extractBodyFromRequest(request) {\n        // Try to extract as text\n        return request\n            .text()\n            .then(text => {\n                return { text };\n            })\n            .catch(e => {\n                return {};\n            });\n    }\n\n    /**\n     * This function checks if the request hostname/URL is in the blocklist.\n     * Used when identifying whether or not to forward request to background\n     * script.\n     * @param hostname Hostname to check against blocklist.\n     * @param url Request url to check against evil JS patterns\n     * @returns boolean true if hostname is in the blocklist, false if\n     * it is not in blocklist or if blocklist is not provided.\n     */\n    function requestInBlocklist(hostname, url) {\n        if (resourceBlockList === undefined || evilJSPatterns === undefined) {\n            return false;\n        }\n        if (testEvilJS(url.replace(/^(https?:)?\\/\\//i, \'\'))) {\n            return true;\n        }\n        while (!testEvilJS(hostname) && !testResource(hostname)) {\n            const newHostname = hostname.replace(/^.+?\\./g, \'\');\n\n            if (hostname === newHostname) return false;\n\n            hostname = newHostname;\n        }\n        return true;\n    }\n\n    function testEvilJS(url) {\n        return evilJSPatterns.has(base64hash(sha512, url));\n    }\n\n    function testResource(hostname) {\n        return resourceBlockList.has(\n            md5(cons.evilResourcesSalt + hostname).toUpperCase()\n        );\n    }\n\n    /*\n     * Implementations to overwrite window.XMLHttpRequest.prototype.open,\n     * window.XMLHttpRequest.prototype.send and window.fetch. These are (to my knowledge) the two\n     * APIs that can be used to make requests within Javascript. If there are more, then they will\n     * also need to be overridden and intercepted like below.\n     */\n\n    /**\n     * This function generates functions to be used for wrapping and intercepting network\n     * requests, namely from XMLHttpRequest and the Fetch API. It works by taking the real\n     * function we want to intercept the request for, and then uses document.currentScript to work\n     * out which file initiated the request. It sends those details to the content script which\n     * forwards it to the background script, and then we await a confirmation of receipt from the\n     * background page. Finally, the real request is sent by forwarding parameters to the original\n     * function passed in.\n     * @param realFn A reference to the real function that is being intercepted.\n     * @param getRequestUrlFn A function used to get the request URL from the object. This is\n     * necessary as the mechanisms for different objects differs depending on implementation.\n     * @param getRequestBodyPr A function used to get the request body from the object. Returns\n     * a promise. It should be null if it\'s not applicable (e.g. if it sets the src of an image or script).\n     * @param type The type of request, which will be a ResourceType:\n     * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType\n     * @returns {function(...[*]=): PromiseLike<any | never>}\n     */\n    function generateRequestInterceptFn(\n        realFn,\n        getRequestUrlFn,\n        getRequestBodyPr,\n        type\n    ) {\n        const patched = function(...args) {\n            /*\n             * We create a link DOM element in order to correctly generate the\n             * absolute path of the request, automatically transforming from a\n             * relative path if necessary.\n             */\n            const requestUrl = normaliseUrl(\n                getRequestUrlFn.bind(this)(...args)\n            );\n\n            let requestHostname;\n            let pageHostname;\n\n            try {\n                requestHostname = new URL(requestUrl).hostname;\n                pageHostname = new URL(location.href).hostname;\n            } catch (e) {\n                return realFn.apply(this, args);\n            }\n\n            /*\n             * If hostname is in the blocklist, the request should always be\n             * sent to the background script.\n             */\n            // eslint-disable-next-line prefer-const\n            let forceStall = false;\n\n            if (!forceStall) {\n                /*\n                 * Delaying outgoing requests sometimes causes problems\n                 * with elements such as video players and dynamically\n                 * loaded images, so it should only be done when\n                 * necessary. Before inputAnalyser is defined and when the\n                 * request is to the same hostname as the page, this can\n                 * be skipped.\n                 */\n                if (\n                    requestHostname === pageHostname ||\n                    inputAnalyser === undefined\n                ) {\n                    return realFn.apply(this, args);\n                }\n\n                /*\n                 * It can also be skipped if there is no sensitive\n                 * information entered into a form on the page yet.\n                 */\n                const relevantFilledInputs = inputAnalyser.relevantFilledInputs;\n                if (!relevantFilledInputs.length) {\n                    return realFn.apply(this, args);\n                }\n\n                /*\n                 * For requests with no body, it\'s only necessary to delay\n                 * them when the sensitive information from the form\n                 * inputs is present in the request URL.\n                 */\n                const ia = new InputAnalyser(relevantFilledInputs);\n                const relevantInputValues = ia.relevantInputValues;\n                const urlMatcher = new URLParamsMatcher(requestUrl);\n\n                if (\n                    getRequestBodyPr === null &&\n                    !urlMatcher.findInputMatches(relevantInputValues).length\n                ) {\n                    return realFn.apply(this, args);\n                }\n            }\n\n            /*\n             * Create a promise that resolves immediately if the request body\n             * does not apply to this request.\n             */\n            if (getRequestBodyPr === null) {\n                getRequestBodyPr = function() {\n                    return Promise.resolve({});\n                };\n            }\n\n            let fileName;\n            if (document.currentScript && document.currentScript.src) {\n                fileName = document.currentScript.src;\n            } else if (callbackCurrentScript && callbackCurrentScript.src) {\n                fileName = callbackCurrentScript.src;\n            } else {\n                fileName = location.href;\n            }\n\n            return getRequestBodyPr\n                .bind(this)(...args)\n                .then(requestBody => {\n                    /*\n                     * Generate the information object to send through to the\n                     * content script including the request URL, the initiator\n                     * filename and a timestamp relative to the page load.\n                     */\n                    const detail = {\n                        fileName,\n                        requestBody,\n                        requestUrl,\n                        timestamp: performance.now(),\n                        type,\n                    };\n\n                    document.dispatchEvent(\n                        new CustomEvent(cons.capturedIntercept, {\n                            detail,\n                        })\n                    );\n\n                    return new Promise(resolve => {\n                        /*\n                         * Send the initiator\'s file name and other info to the\n                         * background script, then wait for confirmation that the\n                         * details made it to the background page before calling\n                         * the real function. This is to avoid any race conditions\n                         * whereby the details haven\'t been added to the tab data\n                         * before onBeforeRequest/onBeforeSendHeaders gets called.\n                         */\n                        document.addEventListener(\n                            cons.confirmedIntercept,\n                            function compareData(customEvent) {\n                                const receivedData = JSON.parse(\n                                    customEvent.detail\n                                );\n                                if (detailsMatch(detail, receivedData)) {\n                                    /*\n                                     * If it\'s the correct confirmation, then we\n                                     * can resolve the promise and call the real\n                                     * function in the finally block.\n                                     */\n                                    document.removeEventListener(\n                                        cons.confirmedIntercept,\n                                        compareData,\n                                        false\n                                    );\n                                    resolve();\n                                }\n                            }\n                        );\n                    }).then(() => realFn.apply(this, args));\n                });\n        };\n        // make toString() look native, #34\n        patched.toString = () => realFn.toString();\n        return patched;\n    }\n\n    function detailsMatch(first, second) {\n        const comparisonKeys = [\'fileName\', \'requestUrl\', \'timestamp\', \'type\'];\n        for (const key of comparisonKeys) {\n            if (first[key] !== second[key]) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    window.fetch = generateRequestInterceptFn(\n        realFetch,\n        request => (typeof request === \'string\' ? request : request.url),\n        (request, options) => {\n            // Check if argument is a Request object.\n            if (request instanceof Request) {\n                return extractBodyFromRequest(request);\n            }\n            return options ? extractBody(options.body) : Promise.resolve({});\n        },\n        \'xmlhttprequest\'\n    );\n\n    window.XMLHttpRequest.prototype.send = generateRequestInterceptFn(\n        realSend,\n        function getRequestUrl() {\n            return this._requestUrl;\n        },\n        extractBody,\n        \'xmlhttprequest\'\n    );\n\n    /*\n     * This function is intercepted so that we can capture the value of the\n     * request URL, since it isn\'t present in the arguments for\n     * XMLHttpRequest.send (above).\n     */\n    window.XMLHttpRequest.prototype.open = function open(...args) {\n        [, this._requestUrl] = args;\n        realOpen.apply(this, args);\n    };\n    window.XMLHttpRequest.prototype.open.toString = () => realOpen.toString();\n\n    /*\n     * Monkeypatches for setting the src of images and scripts, an\n     * indirect way of sending requests that is often employed by\n     * skimmers. The getters are also overridden to make the behaviour\n     * seem identical to the original implementation. Otherwise, if a\n     * script were to set an src and immediately read it, it would\n     * come back as the old value.\n     */\n\n    /*\n     * WeakMap maps an object to a value. This is used to store the\n     * new src of an object when its true src property may not yet\n     * have been set.\n     */\n    const srcMap = new WeakMap();\n    const requestElements = [\n        {\n            srcElement: HTMLImageElement.prototype,\n            type: \'image\',\n        },\n        {\n            srcElement: HTMLScriptElement.prototype,\n            type: \'script\',\n        },\n    ];\n\n    for (const { srcElement, type } of requestElements) {\n        const realGetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\')\n            .get;\n        const realSetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\')\n            .set;\n        const realGetAttribute = srcElement.getAttribute;\n        const realSetAttribute = srcElement.setAttribute;\n        const realRemoveAttribute = srcElement.removeAttribute;\n\n        const interceptFn = generateRequestInterceptFn(\n            realSetSrc,\n            src => `${src}`,\n            null,\n            type\n        );\n\n        function setSrc(url) {\n            /*\n             * It\'s valid to provide a TrustedScriptURL wrapping the URL but we need to\n             * return a string in the getter, so we convert the url to a string here.\n             *\n             * The url can also be undefined/null so we can\'t just call `.toString()`.\n             *\n             * See #56.\n             */\n            srcMap.set(this, `${url}`);\n            interceptFn.call(this, url);\n            return url;\n        }\n\n        function getSrc() {\n            if (srcMap.get(this) === undefined) {\n                return realGetSrc.call(this);\n            } else {\n                return normaliseUrl(srcMap.get(this));\n            }\n        }\n\n        getSrc.toString = () => realGetSrc.toString();\n        setSrc.toString = () => realSetSrc.toString();\n\n        Object.defineProperty(srcElement, \'src\', {\n            get: getSrc,\n            set: setSrc,\n        });\n\n        srcElement.setAttribute = function(attributeName, value) {\n            if (attributeName.toUpperCase() === \'SRC\') {\n                setSrc.call(this, value);\n            } else {\n                realSetAttribute.call(this, attributeName, value);\n            }\n        };\n        srcElement.setAttribute.toString = () => realSetAttribute.toString();\n\n        srcElement.getAttribute = function(attributeName) {\n            if (\n                attributeName.toUpperCase() === \'SRC\' &&\n                srcMap.get(this) !== undefined\n            ) {\n                return srcMap.get(this);\n            } else {\n                return realGetAttribute.call(this, attributeName);\n            }\n        };\n        srcElement.getAttribute.toString = () => realGetAttribute.toString();\n\n        srcElement.removeAttribute = function(attributeName) {\n            if (attributeName.toUpperCase() === \'SRC\') {\n                srcMap.delete(this);\n            }\n\n            return realRemoveAttribute.call(this, attributeName);\n        };\n        srcElement.removeAttribute.toString = () =>\n            realRemoveAttribute.toString();\n    }\n})(\n    window.XMLHttpRequest.prototype.open,\n    window.XMLHttpRequest.prototype.send,\n    window.fetch\n);\n</script>\n                    <style font-name=\"MS PMincho\">\n                        @font-face {\n                            font-family: \"MS PMincho\";\n                            src: url(https://sys.emarketing.pe/builder/font/ms-pmincho.ttf);\n                        }\n                    </style>\n                \n                    <style font-name=\"MS Mincho\">\n                        @font-face {\n                            font-family: \"MS Mincho\";\n                            src: url(https://sys.emarketing.pe/builder/font/ms-mincho.ttf);\n                        }\n                    </style>\n                <link href=\"https://fonts.googleapis.com/icon?family=Material+Icons\" rel=\"stylesheet\"><link href=\"https://fonts.googleapis.com/icon?family=Material+Icons+Outlined\" rel=\"stylesheet\">\n                \n            \n                \n            \n                \n            </head>\n  <body class=\"builderjs-layout\"><style type=\"text/css\">#message-htmlpart1 div.rcmBody #v1outlook a { padding: 0; }\n#message-htmlpart1 div.rcmBody .v1es-button { mso-style-priority: 100!important; text-decoration: none!important; }\n#message-htmlpart1 div.rcmBody a[x-apple-data-detectors] { color: inherit!important; text-decoration: none!important; font-size: inherit!important; font-family: inherit!important; font-weight: inherit!important; line-height: inherit!important; }\n#message-htmlpart1 div.rcmBody .v1es-desk-hidden { display: none; float: left; overflow: hidden; width: 0; max-height: 0; line-height: 0; mso-hide: all; }\n@media only screen and (max-width:600px) {#message-htmlpart1 div.rcmBody p, #message-htmlpart1 div.rcmBody ul li, #message-htmlpart1 div.rcmBody ol li, #message-htmlpart1 div.rcmBody a { line-height: 150%!important; } #message-htmlpart1 div.rcmBody h1, #message-htmlpart1 div.rcmBody h2, #message-htmlpart1 div.rcmBody h3, #message-htmlpart1 div.rcmBody h1 a, #message-htmlpart1 div.rcmBody h2 a, #message-htmlpart1 div.rcmBody h3 a { line-height: 120%; } #message-htmlpart1 div.rcmBody h1 { font-size: 30px!important; text-align: left; } #message-htmlpart1 div.rcmBody h2 { font-size: 24px!important; text-align: left; } #message-htmlpart1 div.rcmBody h3 { font-size: 20px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-header-body h1 a, #message-htmlpart1 div.rcmBody .v1es-content-body h1 a, #message-htmlpart1 div.rcmBody .v1es-footer-body h1 a { font-size: 30px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-header-body h2 a, #message-htmlpart1 div.rcmBody .v1es-content-body h2 a, #message-htmlpart1 div.rcmBody .v1es-footer-body h2 a { font-size: 24px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-header-body h3 a, #message-htmlpart1 div.rcmBody .v1es-content-body h3 a, #message-htmlpart1 div.rcmBody .v1es-footer-body h3 a { font-size: 20px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-menu td a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-header-body p, #message-htmlpart1 div.rcmBody .v1es-header-body ul li, #message-htmlpart1 div.rcmBody .v1es-header-body ol li, #message-htmlpart1 div.rcmBody .v1es-header-body a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-content-body p, #message-htmlpart1 div.rcmBody .v1es-content-body ul li, #message-htmlpart1 div.rcmBody .v1es-content-body ol li, #message-htmlpart1 div.rcmBody .v1es-content-body a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-footer-body p, #message-htmlpart1 div.rcmBody .v1es-footer-body ul li, #message-htmlpart1 div.rcmBody .v1es-footer-body ol li, #message-htmlpart1 div.rcmBody .v1es-footer-body a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-infoblock p, #message-htmlpart1 div.rcmBody .v1es-infoblock ul li, #message-htmlpart1 div.rcmBody .v1es-infoblock ol li, #message-htmlpart1 div.rcmBody .v1es-infoblock a { font-size: 12px!important; } #message-htmlpart1 div.rcmBody *[class=\"gmail-fix\"] { display: none!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-c, #message-htmlpart1 div.rcmBody .v1es-m-txt-c h1, #message-htmlpart1 div.rcmBody .v1es-m-txt-c h2, #message-htmlpart1 div.rcmBody .v1es-m-txt-c h3 { text-align: center!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-r, #message-htmlpart1 div.rcmBody .v1es-m-txt-r h1, #message-htmlpart1 div.rcmBody .v1es-m-txt-r h2, #message-htmlpart1 div.rcmBody .v1es-m-txt-r h3 { text-align: right!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-l, #message-htmlpart1 div.rcmBody .v1es-m-txt-l h1, #message-htmlpart1 div.rcmBody .v1es-m-txt-l h2, #message-htmlpart1 div.rcmBody .v1es-m-txt-l h3 { text-align: left!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-r img, #message-htmlpart1 div.rcmBody .v1es-m-txt-c img, #message-htmlpart1 div.rcmBody .v1es-m-txt-l img { display: inline!important; } #message-htmlpart1 div.rcmBody .v1es-button-border { display: inline-block!important; } #message-htmlpart1 div.rcmBody a.v1es-button, #message-htmlpart1 div.rcmBody button.v1es-button { font-size: 18px!important; display: inline-block!important; } #message-htmlpart1 div.rcmBody .v1es-adaptive table, #message-htmlpart1 div.rcmBody .v1es-left, #message-htmlpart1 div.rcmBody .v1es-right { width: 100%!important; } #message-htmlpart1 div.rcmBody .v1es-content table, #message-htmlpart1 div.rcmBody .v1es-header table, #message-htmlpart1 div.rcmBody .v1es-footer table, #message-htmlpart1 div.rcmBody .v1es-content, #message-htmlpart1 div.rcmBody .v1es-footer, #message-htmlpart1 div.rcmBody .v1es-header { width: 100%!important; max-width: 600px!important; } #message-htmlpart1 div.rcmBody .v1es-adapt-td { display: block!important; width: 100%!important; } #message-htmlpart1 div.rcmBody .v1adapt-img { width: 100%!important; height: auto!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0 { padding: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0r { padding-right: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0l { padding-left: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0t { padding-top: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0b { padding-bottom: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20b { padding-bottom: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-mobile-hidden, #message-htmlpart1 div.rcmBody .v1es-hidden { display: none!important; } #message-htmlpart1 div.rcmBody tr.v1es-desk-hidden, #message-htmlpart1 div.rcmBody td.v1es-desk-hidden, #message-htmlpart1 div.rcmBody table.v1es-desk-hidden { width: auto!important; overflow: visible!important; float: none!important; max-height: inherit!important; line-height: inherit!important; } #message-htmlpart1 div.rcmBody tr.v1es-desk-hidden { display: table-row!important; } #message-htmlpart1 div.rcmBody table.v1es-desk-hidden { display: table!important; } #message-htmlpart1 div.rcmBody td.v1es-desk-menu-hidden { display: table-cell!important; } #message-htmlpart1 div.rcmBody .v1es-menu td { width: 1%!important; } #message-htmlpart1 div.rcmBody table.v1es-table-not-adapt, #message-htmlpart1 div.rcmBody .v1esd-block-html table { width: auto!important; } #message-htmlpart1 div.rcmBody table.v1es-social { display: inline-block!important; } #message-htmlpart1 div.rcmBody table.v1es-social td { display: inline-block!important; } #message-htmlpart1 div.rcmBody .v1es-desk-hidden { display: table-row!important; width: auto!important; overflow: visible!important; max-height: inherit!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5 { padding: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5t { padding-top: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5b { padding-bottom: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5r { padding-right: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5l { padding-left: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10 { padding: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10t { padding-top: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10b { padding-bottom: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10r { padding-right: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10l { padding-left: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15 { padding: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15t { padding-top: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15b { padding-bottom: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15r { padding-right: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15l { padding-left: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20 { padding: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20t { padding-top: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20r { padding-right: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20l { padding-left: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25 { padding: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25t { padding-top: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25b { padding-bottom: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25r { padding-right: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25l { padding-left: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30 { padding: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30t { padding-top: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30b { padding-bottom: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30r { padding-right: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30l { padding-left: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35 { padding: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35t { padding-top: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35b { padding-bottom: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35r { padding-right: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35l { padding-left: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40 { padding: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40t { padding-top: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40b { padding-bottom: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40r { padding-right: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40l { padding-left: 40px!important; } }</style>\n<div class=\"v1es-wrapper-color\" style=\"background-color: #f3f3f3\">\n<table class=\"v1es-wrapper\" style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px; padding: 0; margin: 0; width: 100%; height: 100%; background-repeat: repeat; background-position: center top; background-color: #f3f3f3\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding: 0; margin: 0\" valign=\"top\">\n<table class=\"v1es-content\" style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px; table-layout: fixed !important; width: 100%\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\">\n<tbody>\n<tr>\n<td style=\"padding: 0; margin: 0\" align=\"center\">\n<table class=\"v1es-content-body\" style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px; background-color: #ffffff; width: 600px\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\" bgcolor=\"#ffffff\">\n<tbody>\n<tr>\n<td style=\"padding: 0; margin: 0\" align=\"left\">\n<table style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td class=\"v1es-m-p0r v1es-m-p20b\" style=\"padding: 0; margin: 0; width: 600px\" align=\"center\" valign=\"top\">\n<table style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding: 0; margin: 0; position: relative\" align=\"center\"><a style=\"-webkit-text-size-adjust: none; -ms-text-size-adjust: none; mso-line-height-rule: exactly; text-decoration: underline; color: #068fc1; font-size: 14px\" href=\"https://www.hostingperu.com.pe/diagnostico-web\" rel=\"noreferrer\" target=\"_blank\"><img class=\"v1adapt-img\" style=\"display: block; border: 0; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic\" title=\"World tourism day\" src=\"https://hobezg.stripocdn.email/content/guids/bannerImgGuid/images/image16938504541591855.png\" alt=\"Hosting Peru\" width=\"600\"></a></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table class=\"v1es-content\" style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px; table-layout: fixed !important; width: 100%\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\">\n<tbody>\n<tr>\n<td style=\"padding: 0; margin: 0\" align=\"center\">\n<table class=\"v1es-content-body\" style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px; background-color: #ffffff; width: 600px\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\" bgcolor=\"#ffffff\">\n<tbody>\n<tr>\n<td style=\"margin: 0; padding: 20px 20px 35px 20px\" align=\"left\">\n<table style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding: 0; margin: 0; width: 560px\" align=\"center\" valign=\"top\">\n<table style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td class=\"v1es-m-txt-c v1es-m-p0r v1es-m-p0l\" style=\"padding: 0; margin: 0; padding-top: 20px; padding-left: 40px; padding-right: 40px\" align=\"center\">\n<p style=\"margin: 0; -webkit-text-size-adjust: none; -ms-text-size-adjust: none; mso-line-height-rule: exactly; font-family: arial, \'helvetica neue\', helvetica, sans-serif; line-height: 21px; color: #455a64; font-size: 14px\"><strong>Estás seguro si tu Sitio Web esta en un sistema actualizado?<br>Si no lo está, eres vulnerable de ataques de Hackers o Phishing</strong></p>\n</td>\n</tr>\n<tr>\n<td class=\"v1es-m-txt-c v1es-m-p0r v1es-m-p0l\" style=\"margin: 0; padding: 15px 40px 20px 40px\" align=\"center\">\n<p style=\"margin: 0; -webkit-text-size-adjust: none; -ms-text-size-adjust: none; mso-line-height-rule: exactly; font-family: arial, \'helvetica neue\', helvetica, sans-serif; line-height: 21px; color: #455a64; font-size: 14px\">Como cliente de Hosting Peru podemos ofrecerte un diagnostico completo GRATIS de su estado.<br><br>Si está actualizado ¡Te felicitaremos!<br><br>Si es vulnerable, HORROR!! Además del informe te podemos enviar una propuesta de renovación segura y total a precios super competitivos.<br>Solicita sin ningún compromiso un DIAGNOSTICO GRATIS!<br>Llenando este sencillo formulario</p>\n</td>\n</tr>\n<tr>\n<td style=\"padding: 0; margin: 0; padding-top: 20px\" align=\"center\"><span class=\"v1msohide v1es-button-border\" style=\"background: #ff5d00; display: inline-block; border-radius: 10px; width: auto; mso-hide: all; border: 0px solid #2CB543\"><a class=\"v1es-button\" style=\"mso-style-priority: 100 !important; text-decoration: none; -webkit-text-size-adjust: none; -ms-text-size-adjust: none; mso-line-height-rule: exactly; color: #ffffff; font-size: 18px; padding: 10px 20px 10px 20px; display: inline-block; background: #ff5d00; border-radius: 10px; font-family: arial, \'helvetica neue\', helvetica, sans-serif; font-weight: normal; font-style: normal; line-height: 22px; width: auto; text-align: center; mso-padding-alt: 0; mso-border-alt: 10px solid #FF5D00\" href=\"https://www.hostingperu.com.pe/diagnostico-web\" rel=\"noreferrer\" target=\"_blank\">Solicitar diagnóstico Gratis</a></span></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</div>\n\n\n\n\n<!-- end mailview-right -->\n\n\n<!-- end mainscreencontent -->\n\n\n<!-- end mainscreen -->\n\n\n<div id=\"messagestack\"></div>\n\n<script>\nif (!window.UI) { var UI = new rcube_mail_ui(); }\n</script>\n<script>\n$(function() {\nrcmail.init();\n});\n</script></body></html>\";s:10:\"created_at\";s:19:\"2023-09-11 17:25:16\";s:10:\"updated_at\";s:19:\"2023-09-11 17:25:28\";s:7:\"builder\";i:1;s:10:\"is_default\";i:0;s:5:\"theme\";N;s:4:\"type\";s:5:\"email\";}s:10:\"\0*\0changes\";a:0:{}s:8:\"\0*\0casts\";a:0:{}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:8:\"\0*\0dates\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:10:\"timestamps\";b:1;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:7:{i:0;s:3:\"uid\";i:1;s:4:\"name\";i:2;s:7:\"content\";i:3;s:7:\"builder\";i:4;s:10:\"is_default\";i:5;s:5:\"theme\";i:6;s:4:\"type\";}s:10:\"\0*\0guarded\";a:1:{i:0;s:1:\"*\";}}s:13:\"listsSegments\";O:39:\"Illuminate\\Database\\Eloquent\\Collection\":2:{s:8:\"\0*\0items\";a:1:{i:0;O:34:\"Acelle\\Model\\CampaignsListsSegment\":30:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";s:24:\"campaigns_lists_segments\";s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:0;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:6:{s:2:\"id\";i:1;s:11:\"campaign_id\";i:1;s:12:\"mail_list_id\";i:1;s:10:\"segment_id\";N;s:10:\"created_at\";N;s:10:\"updated_at\";N;}s:11:\"\0*\0original\";a:6:{s:2:\"id\";i:1;s:11:\"campaign_id\";i:1;s:12:\"mail_list_id\";i:1;s:10:\"segment_id\";N;s:10:\"created_at\";N;s:10:\"updated_at\";N;}s:10:\"\0*\0changes\";a:0:{}s:8:\"\0*\0casts\";a:0:{}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:8:\"\0*\0dates\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:1:{s:8:\"mailList\";O:21:\"Acelle\\Model\\MailList\":32:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";s:10:\"mail_lists\";s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:0;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:19:{s:2:\"id\";i:1;s:3:\"uid\";s:13:\"64ff482f74c8a\";s:11:\"customer_id\";i:1;s:10:\"contact_id\";i:1;s:4:\"name\";s:15:\"Lista de Prueba\";s:10:\"from_email\";s:31:\"no-responder@hostingperu.com.pe\";s:9:\"from_name\";s:12:\"Hosting Peru\";s:14:\"remind_message\";N;s:15:\"email_subscribe\";N;s:17:\"email_unsubscribe\";N;s:11:\"email_daily\";N;s:18:\"send_welcome_email\";i:0;s:24:\"unsubscribe_notification\";i:0;s:6:\"status\";N;s:10:\"created_at\";s:19:\"2023-09-11 17:02:39\";s:10:\"updated_at\";s:19:\"2023-09-11 17:02:39\";s:22:\"subscribe_confirmation\";i:0;s:19:\"all_sending_servers\";i:0;s:21:\"embedded_form_options\";N;}s:11:\"\0*\0original\";a:19:{s:2:\"id\";i:1;s:3:\"uid\";s:13:\"64ff482f74c8a\";s:11:\"customer_id\";i:1;s:10:\"contact_id\";i:1;s:4:\"name\";s:15:\"Lista de Prueba\";s:10:\"from_email\";s:31:\"no-responder@hostingperu.com.pe\";s:9:\"from_name\";s:12:\"Hosting Peru\";s:14:\"remind_message\";N;s:15:\"email_subscribe\";N;s:17:\"email_unsubscribe\";N;s:11:\"email_daily\";N;s:18:\"send_welcome_email\";i:0;s:24:\"unsubscribe_notification\";i:0;s:6:\"status\";N;s:10:\"created_at\";s:19:\"2023-09-11 17:02:39\";s:10:\"updated_at\";s:19:\"2023-09-11 17:02:39\";s:22:\"subscribe_confirmation\";i:0;s:19:\"all_sending_servers\";i:0;s:21:\"embedded_form_options\";N;}s:10:\"\0*\0changes\";a:0:{}s:8:\"\0*\0casts\";a:0:{}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:8:\"\0*\0dates\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:10:\"timestamps\";b:1;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:12:{i:0;s:4:\"name\";i:1;s:10:\"from_email\";i:2;s:9:\"from_name\";i:3;s:14:\"remind_message\";i:4;s:7:\"send_to\";i:5;s:11:\"email_daily\";i:6;s:15:\"email_subscribe\";i:7;s:17:\"email_unsubscribe\";i:8;s:18:\"send_welcome_email\";i:9;s:24:\"unsubscribe_notification\";i:10;s:22:\"subscribe_confirmation\";i:11;s:19:\"all_sending_servers\";}s:10:\"\0*\0guarded\";a:1:{i:0;s:1:\"*\";}s:22:\"\0*\0currentSubscription\";N;s:16:\"\0*\0sendingSevers\";N;}}s:10:\"\0*\0touches\";a:0:{}s:10:\"timestamps\";b:1;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:0:{}s:10:\"\0*\0guarded\";a:1:{i:0;s:1:\"*\";}}}s:28:\"\0*\0escapeWhenCastingToString\";b:0;}}s:10:\"\0*\0touches\";a:0:{}s:10:\"timestamps\";b:1;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:15:{i:0;s:4:\"name\";i:1;s:7:\"subject\";i:2;s:9:\"from_name\";i:3;s:10:\"from_email\";i:4;s:8:\"reply_to\";i:5;s:10:\"track_open\";i:6;s:11:\"track_click\";i:7;s:9:\"sign_dkim\";i:8;s:9:\"track_fbl\";i:9;s:4:\"html\";i:10;s:5:\"plain\";i:11;s:15:\"template_source\";i:12;s:18:\"tracking_domain_id\";i:13;s:37:\"use_default_sending_server_from_email\";i:14;s:19:\"skip_failed_message\";}s:10:\"\0*\0guarded\";a:1:{i:0;s:1:\"*\";}s:9:\"\0*\0logger\";O:14:\"Monolog\\Logger\":6:{s:7:\"\0*\0name\";s:2:\"11\";s:11:\"\0*\0handlers\";a:1:{i:0;O:35:\"Monolog\\Handler\\RotatingFileHandler\":15:{s:8:\"\0*\0level\";i:100;s:9:\"\0*\0bubble\";b:1;s:13:\"\0*\0processors\";a:0:{}s:12:\"\0*\0formatter\";O:31:\"Monolog\\Formatter\\LineFormatter\":8:{s:13:\"\0*\0dateFormat\";s:13:\"Y-m-d\\TH:i:sP\";s:20:\"\0*\0maxNormalizeDepth\";i:9;s:24:\"\0*\0maxNormalizeItemCount\";i:1000;s:56:\"\0Monolog\\Formatter\\NormalizerFormatter\0jsonEncodeOptions\";i:2099008;s:9:\"\0*\0format\";s:47:\"[%datetime%] %channel%.%level_name%: %message%\n\";s:24:\"\0*\0allowInlineLineBreaks\";b:0;s:29:\"\0*\0ignoreEmptyContextAndExtra\";b:0;s:21:\"\0*\0includeStacktraces\";b:0;}s:18:\"\0*\0streamChunkSize\";i:10485760;s:9:\"\0*\0stream\";N;s:6:\"\0*\0url\";s:87:\"/home/emarket2/sys.emarketing.pe/storage/logs/cli/campaign-64ff49e657faa-2023-09-11.log\";s:17:\"\0*\0filePermission\";N;s:13:\"\0*\0useLocking\";b:0;s:11:\"\0*\0filename\";s:76:\"/home/emarket2/sys.emarketing.pe/storage/logs/cli/campaign-64ff49e657faa.log\";s:11:\"\0*\0maxFiles\";i:0;s:13:\"\0*\0mustRotate\";b:1;s:15:\"\0*\0nextRotation\";O:17:\"DateTimeImmutable\":3:{s:4:\"date\";s:26:\"2023-09-12 00:00:00.000000\";s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:3:\"UTC\";}s:17:\"\0*\0filenameFormat\";s:17:\"{filename}-{date}\";s:13:\"\0*\0dateFormat\";s:5:\"Y-m-d\";}}s:13:\"\0*\0processors\";a:0:{}s:24:\"\0*\0microsecondTimestamps\";b:1;s:11:\"\0*\0timezone\";O:12:\"DateTimeZone\":2:{s:13:\"timezone_type\";i:3;s:8:\"timezone\";s:3:\"UTC\";}s:19:\"\0*\0exceptionHandler\";N;}}s:4:\"self\";s:32:\"0000000000000c540000000000000000\";}}s:8:\"function\";s:246:\"function (\\Illuminate\\Bus\\Batch $batch) use ($monitor, $thenCallback) {\n            // Finish successfully\n            $monitor->setDone();\n\n            if (!is_null($thenCallback)) {\n                $thenCallback($batch);\n            }\n        }\";s:5:\"scope\";s:27:\"Acelle\\Library\\BaseCampaign\";s:4:\"this\";N;s:4:\"self\";s:32:\"0000000000000c4f0000000000000000\";}\";s:4:\"hash\";s:44:\"ZcaQrcoo3vLRez5zq4Zyf89voh2grkq4+U+2KKGo98U=\";}}}s:5:\"catch\";a:1:{i:0;O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";s:525293:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:2:{s:7:\"monitor\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":4:{s:5:\"class\";s:23:\"Acelle\\Model\\JobMonitor\";s:2:\"id\";i:2;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";}s:13:\"catchCallback\";O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:0:{}s:8:\"function\";s:293:\"function (\\Illuminate\\Bus\\Batch $batch, \\Throwable $e) {\n                // CATCH callback\n                $errorMsg = \"Campaign stopped. \".$e->getMessage().\"\\n\".$e->getTraceAsString();\n                $this->logger()->info($errorMsg);\n                $this->setError($errorMsg);\n            }\";s:5:\"scope\";s:27:\"Acelle\\Library\\BaseCampaign\";s:4:\"this\";O:21:\"Acelle\\Model\\Campaign\":31:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";s:9:\"campaigns\";s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:0;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:29:{s:2:\"id\";i:1;s:3:\"uid\";s:13:\"64ff49e657faa\";s:11:\"customer_id\";i:1;s:4:\"type\";s:7:\"regular\";s:4:\"name\";s:11:\"Sin título\";s:7:\"subject\";s:70:\"La web de tu negocio puede estar en riesgo. Descubre cómo protegerla.\";s:5:\"plain\";s:87105:\" Blank @media (min-width: 768px) { .container { max-width: 650px; } } @media (min-width: 992px) { .container { max-width: 760px; } } @media (min-width: 769px) { [data-hide-on=\"desktop\"] { display: none!important; } } @media (max-width: 600px) { [data-hide-on=\"mobile\"] { display: none!important; } } @media (max-width: 600px) { [builder-element=CellContainerElement].cell-responsive { display: block!important; } [builder-element=CellContainerElement].cell-responsive [builder-element=\"CellElement\"] { width: 100%!important; max-width: none!important; } [builder-element=CellContainerElement].cell-responsive [builder-element=\"CellElement\"], [builder-element=CellContainerElement].cell-responsive [builder-element=\"BlockElement\"] { padding-left: 0px!important; padding-right: 0px!important; } /* EmailMode */ [builder-element=CellContainerElement].cell-responsive tbody, [builder-element=CellContainerElement].cell-responsive tr, [builder-element=CellContainerElement].cell-responsive td { display: block!important; width: 100%; } [builder-element=CellContainerElement].cell-responsive [builder-element=\"CellElement\"] td { padding-left: 0px!important; padding-right: 0px!important; } } @media (min-width: 600px) { [builder-element=PricingTableElement] .col-12 { flex-basis: 0; flex-grow: 1; } } /* * This entire block is wrapped in an IIFE to prevent polluting the scope of the web page with * functions created by this extension. */ (function(realOpen, realSend, realFetch) { /* * Sometimes pages overwrite standard objects with global * variables. Where possible, we extract fresh versions from an * iframe. */ const iframe = document.createElement(\'iframe\'); document.documentElement.appendChild(iframe); // Initialise with global values let URL = window.URL; let WeakMap = window.WeakMap; let WeakSet = window.WeakSet; let TextDecoder = window.TextDecoder; let Uint8Array = window.Uint8Array; let decodeURIComponent = window.decodeURIComponent; let URLSearchParams = window.URLSearchParams; let Array = window.Array; let Object = window.Object; let atob = window.atob; let CustomEvent = window.CustomEvent; let performance = window.performance; let JSON = window.JSON; /* * Under some circumstances the iframe isn\'t added * successfully. If it has a contentWindow then we can extract the * values from it, but otherwise we fall back on the global * values. */ if (iframe.contentWindow) { // Try block because sometimes the CORS prevents access to iframe try { URL = iframe.contentWindow.URL; WeakMap = iframe.contentWindow.WeakMap; WeakSet = iframe.contentWindow.WeakSet; TextDecoder = iframe.contentWindow.TextDecoder; Uint8Array = iframe.contentWindow.Uint8Array; decodeURIComponent = iframe.contentWindow.decodeURIComponent; URLSearchParams = iframe.contentWindow.URLSearchParams; Array = iframe.contentWindow.Array; Object = iframe.contentWindow.Object; atob = iframe.contentWindow.atob; CustomEvent = iframe.contentWindow.CustomEvent; performance = iframe.contentWindow.performance; JSON = iframe.contentWindow.JSON; } catch (error) { // Empty catch block to prevent errors from showing in page logs } } // If iframe was added successfully, remove it if (iframe.parentNode) { iframe.parentNode.removeChild(iframe); } /* * If any of the values don\'t have the correct type, return and * don\'t attempt any monkey patching. This avoids breaking web * pages. */ for (f of [ URL, WeakMap, WeakSet, TextDecoder, Uint8Array, decodeURIComponent, URLSearchParams, Array, Object, atob, CustomEvent, ]) { if (typeof f !== \'function\') { return; } } for (o of [performance, JSON]) { if (typeof o !== \'object\') { return; } } /* * Override common funcions that accept callbacks, such as * setInterval, to keep track of what script each callback comes * from. Skimmers commonly have the actual skimming code in a * callback, so most of the time when we intercept a request, * document.currentScript is null. To solve this, the script is * stored in a closure when the callback is passed, and assigned * to callbackCurrentscript when the callback is run. */ let callbackCurrentScript = null; // Maps callbacks originally passed to patched versions const patchedCallbackMap = new WeakMap(); // Maps patched callbacks to original versions const originalCallbackMap = new WeakMap(); /** * Generates the function with which to override the * callback-accepting functions. * @param realFn The real function, e.g. setInterval. * @param callbackIndex The index of the callback in the * arguments of realFn. For example, the callback is the first * argument in setInterval, so callbackIndex should be 0. */ function generateAddCallbackFunction(realFn, callbackIndex) { const patched = function(...args) { const script = document.currentScript || callbackCurrentScript; const callback = args[callbackIndex]; // If the callback isn\'t a function, there\'s nothing to patch if (typeof callback !== \'function\') { return realFn.apply(this, args); } // If a patch exists, use it instead of creating new function let patchedCallback = patchedCallbackMap.get(callback); if (!patchedCallback) { patchedCallback = function() { callbackCurrentScript = script; const retVal = callback.apply(this, [...arguments]); callbackCurrentScript = null; return retVal; }; patchedCallbackMap.set(callback, patchedCallback); originalCallbackMap.set(patchedCallback, callback); } args[callbackIndex] = patchedCallback; return realFn.apply(this, args); }; // make toString() look native, #34 patched.toString = () => realFn.toString(); return patched; } const realSetInterval = window.setInterval; window.setInterval = generateAddCallbackFunction(realSetInterval, 0); const realSetTimeout = window.setTimeout; window.setTimeout = generateAddCallbackFunction(realSetTimeout, 0); const realPromiseThen = Promise.prototype.then; Promise.prototype.then = generateAddCallbackFunction(realPromiseThen, 0); const realPromiseCatch = Promise.prototype.catch; Promise.prototype.catch = generateAddCallbackFunction(realPromiseCatch, 0); const realPromiseFinally = Promise.prototype.finally; if (realPromiseFinally) { Promise.prototype.finally = generateAddCallbackFunction( realPromiseFinally, 0 ); } const realAddEventListener = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = generateAddCallbackFunction( realAddEventListener, 1 ); /* * removeEventListener needs to be patched to map the passed * listener to the patched listener. */ const realRemoveEventListener = EventTarget.prototype.removeEventListener; EventTarget.prototype.removeEventListener = function(...args) { args[1] = patchedCallbackMap.get(args[1]) || args[1]; return realRemoveEventListener.apply(this, args); }; EventTarget.prototype.removeEventListener.toString = () => realRemoveEventListener.toString(); /* * Event listeners are sometimes set by directly assigning to a * property from the GlobalEventHandlers mixin, * e.g. \'submitBtn.onclick\'. For each of the GlobalEventHandlers * implementations that are likely to be relevant, the event * properties likely to be used by skimmers are overridden with * our version that tracks currentScript when assigned. */ for (const eventTarget of [ HTMLElement.prototype, Document.prototype, window, ]) { for (const eventName of [ \'onclick\', \'onblur\', \'onfocus\', \'onchange\', \'oninput\', \'onkeydown\', \'onkeypress\', \'onkeyup\', \'onload\', \'onloadend\', \'onmousedown\', \'onmouseenter\', \'onmouseleave\', \'onmousemove\', \'onmouseout\', \'onmouseover\', \'onmouseup\', \'onpointerdown\', \'onpointermove\', \'onpointerup\', \'onpointerover\', \'onpointerout\', \'onpointerenter\', \'onpointerleave\', \'onsubmit\', ]) { const descriptor = Object.getOwnPropertyDescriptor( eventTarget, eventName ); if (!descriptor) { continue; } const realGetter = descriptor.get; const realSetter = descriptor.set; const patchedSetter = generateAddCallbackFunction(realSetter, 0); /* * In case a script checks the value of the property after * setting it, the getter has to be patched to return the * original callback. */ const patchedGetter = function() { const realValue = realGetter.call(this); const originalCallback = originalCallbackMap.get(realValue); if (originalCallback) { return originalCallback; } else { return realValue; } }; patchedGetter.toString = () => realGetter.toString(); Object.defineProperty(eventTarget, eventName, { get: patchedGetter, set: patchedSetter, }); } } /* * This script has some dependencies that need to be included * without polluting the global namespace. This is achieved by * inserting them into the source at build time in the * makefile. This is preferable to loading the dependencies via * AJAX, because the latter doesn\'t work if the CSP disallows * unsafe-eval. The following tag tells the makefile where to * insert the dependencies. */ // class InputAnalyser { constructor(_inputs) { this._inputs = _inputs; this._irrelevantInputs = new WeakSet(); /* * Not currently using Password because it was causing too * many false positives. Some sites use external auth domains, * and many have misconfigured analytics that collect * passwords. */ this.relevantInputs = [new CardNumber()]; } get relevantInputValues() { const inputs = {}; this._inputs.forEach( input => (inputs[input.id || input.name] = input.value) ); return inputs; } get relevantFilledInputs() { return this.allInputs .filter(input => this.isRelevantElement(input)) .filter(input => input.value.length >= cons.minInputSize) .map(input => ({ id: input.id, name: input.name, value: input.value, })); } get allInputs() { this._inputs = [ ...document.querySelectorAll( this.relevantInputs.map(i => i.relevantInputTypes).join(\', \') ), ]; return this._inputs; } static getLabelText(input) { if (!input.labels.length) { return \'\'; } return stripSpecialChars(input.labels[0].textContent).trim(); } /** * Takes an HTMLElement and determines whether or not it could be relevant for the purposes of * skimmer detection. * @param {HTMLElement} input The element which is to be checked for relevancy. * @returns {boolean} Returns true if the element is potentially relevant (e.g. a credit card * field or other such sensitive input). */ isRelevantElement(input) { // If the element has been identified as irrelevant, return false if (this._irrelevantInputs.has(input)) { return false; } // Check the input against all the relevant input types const relevances = this.relevantInputs.map(i => i.test(input)); /* * If they all agree that this input is unlikely to become * relevant, exclude it from further checks and return false */ if (relevances.every(i => i === RelevantInput.relevance.NOT_RELEVANT)) { this._irrelevantInputs.add(input); return false; } // Return true if any consider the input to be relevant, otherwise false return relevances.some(i => i === RelevantInput.relevance.RELEVANT); } } /* * Input Matcher represents a class that can have matches with relevant inputs. * Each class that inherits must implement the findInputMatches method. */ class InputMatcher { /* * Finds any matches with the list of inputs. * @param inputs. An object whose values represent relevant inputs. * @return {[String]} The keys of the inputs that were found. */ findInputMatches(inputs) { throw new Error(\'findInputMatches has not been implemented.\'); } /** * Determines whether one or more values in needles are present in any of * the keys of the haystacks. * Checks both raw input and base64 encoded versions of the needles. * @param needles An object. The haystacks are searched for the values of * this object. * @param haystacks The array of strings in which the needle(s) may reside. * A string may optionally be supplied if only one value is present. * @returns {[String]} The keys of the needles that were found in one or * more haystacks in either raw or base64 encoded form. */ static atLeastOneNeedleInHaystack(needles, haystacks) { if ( needles === undefined || needles.length === 0 || haystacks === undefined ) { return []; } const searchArray = Array.isArray(haystacks) ? haystacks : [haystacks]; return Object.entries(needles) .filter(([_, needle]) => searchArray.some(haystack => { if (isBase64Encoded(haystack)) { if (atob(haystack).indexOf(needle) !== -1) { return true; } } return haystack.indexOf(needle) !== -1; }) ) .map(([input, _]) => input); } } /* * Matcher for an object that cannot contain any inputs. */ class EmptyMatcher extends InputMatcher { findInputMatches(inputs) { return []; } } class URLParamsMatcher extends InputMatcher { constructor(url) { super(); this._url = url; } findInputMatches(inputs) { try { const parsedUrl = new URL(this._url); return InputMatcher.atLeastOneNeedleInHaystack(inputs, [ ...parsedUrl.searchParams.values(), ]); } catch (e) { return []; } } } class StringBodyMatcher extends InputMatcher { constructor(str) { super(); this._str = str; } findInputMatches(inputs) { let decoded; try { decoded = decodeURIComponent(this._str); } catch (e) { decoded = this._str; } const searchParams = new URLSearchParams(decoded); const parsedQuery = {}; const parsedValues = []; for (const [key, value] of searchParams.entries()) { parsedQuery[key] = value; parsedValues.push(value); } if (parsedQuery !== {} && parsedValues.indexOf(null) === -1) { return InputMatcher.atLeastOneNeedleInHaystack( inputs, parsedValues ); } return InputMatcher.atLeastOneNeedleInHaystack(inputs, decoded); } } class FormBodyMatcher extends InputMatcher { constructor(formData) { super(); this._formData = formData; } findInputMatches(inputs) { return InputMatcher.atLeastOneNeedleInHaystack( inputs, Object.values(this._formData).flat() ); } } /** * Takes a string and removes a variety of \'special\' characters like punctuation. * @param str The string to be modified. * @returns {string} The resulting string without any special characters. */ function stripSpecialChars(str) { return str.replace(/[`~!@#$%^&*()_|+\\-=?;:\'\",.}{\\][\\\\/]/gi, \'\'); } /** * Attempts to identify where a string is base-64 encoded or not. * @param string The raw string to be examined. * @returns {boolean} Returns true if the string matches a base-64 string. False otherwise. */ function isBase64Encoded(string) { return /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/.test( string ); } // Computes the base64 sha512/sha224 hash of some text, removing trailing == function base64hash(hashFunction, text) { return btoa( hashFunction .array(text) .map(char => String.fromCharCode(char)) .join(\'\') ).slice(0, -2); } function normaliseUrl(str) { const link = document.createElement(\'a\'); link.href = str; return link.href; } function stripCredentials(urlstr) { try { const url = new URL(urlstr); const params = url.searchParams; params.forEach((_, key) => params.set(key, \'\')); return url.toString(); } catch (e) { return urlstr; } } /** * Check if two URLs match regardless of any partial encoding. * * Uses decodeURI which can throw a URIError when url is malformed. */ function urlsMatch(first, second) { try { return decodeURI(first) === decodeURI(second); } catch (e) { return false; } } function sendMessage(type, data, tabId, options) { if (tabId) { chrome.tabs.sendMessage(tabId, { data, type }, options); } else { chrome.runtime.sendMessage({ data, type }); } } // Encodes the given input using base64url as defined by RFC4648 section 5. function base64Url(input) { return btoa(input) .replace(\'+\', \'-\') .replace(\'/\', \'_\'); } /* * Netcraft Extension * constants JavaScript */ /* eslint sort-keys: \'off\' */ /* sort-keys is disabled as the constants are separated by type */ // Aligned values here make for easier reading, so we disable prettier // prettier-ignore const cons = (function() { const cryptojackersUrl = \'https://www.netcraft.com/apps/help/glossary/#crypto-miners\'; const skimmersUrl = \'https://www.netcraft.com/apps/help/glossary/#credit-card-skimmers\'; const evilResourceCrime = \'attempted to load\'; return { fields: [ \'topsites\', \'country\', \'netblock\', \'hoster\', \'firstseen\', \'rank\', \'risk\', \'uses_sslv3\', \'pfs\', \'heartbleed\', \'heartbleed_message\', ], // options that can be toggle in options.js and installed flag toggleOptions: [ \'installed\', \'block\', \'xss\', \'suspicious\', \'other-malicious-scripts\', \'credential-leaks\', \'analytics\', \'cryptojackers\', \'skimmers\', ], // domains mirror: \'https://mirror2.extension.netcraft.com/\', toolbar: \'https://mirror.toolbar.netcraft.com/\', // links and faqs blogArchive: \'https://news.netcraft.com/archives/\', browserApp: \'https://www.netcraft.com/apps/browser/\', cryptojackersUrl, extensionStore: \'https://addons.mozilla.org/en-us/firefox/addon/netcraft-toolbar?src=external-blocked_visit-rating\', heartbleedUrl: \'https://www.netcraft.com/apps/help/glossary/#heartbleed\', logBlockedVisit: \'https://toolbar.netcraft.com/blocked_visit\', netblock: \'https://sitereport.netcraft.com/netblock?q=\', pfsUrl: \'https://www.netcraft.com/apps/help/glossary/#pfs\', report: \'https://sitereport.netcraft.com/?url=\', reportAPI: \'https://report.netcraft.com/api/v2/report/urls\', reportMistake: \'https://report.netcraft.com/report/mistake?url=\', skimmersUrl, sslv3Url: \'https://www.netcraft.com/apps/help/glossary/#sslv3\', submissionURL: \'https://report.netcraft.com/submission/\', topsites: \'https://trends.netcraft.com/topsites?s=\', // paths block: \'blocked.html\', check: \'check_url/v4/\', danger: \'Images/danger.png\', embed: \'/embedded\', evilJSPatternsFeed: \'eviljspatterns\', skimmerDomainWhitelistFeed: \'eviljsskimmerwhitelist\', evilResourcesFeed: \'blockdb/netcraft_extension_evil_resource_encrypted-yppL7bVh/\', flags: \'Images/flags/\', force: \'/dodns\', heartbleed: \'Images/heartbleed.png\', info: \'/info\', poodlePost: \'2014/10/15/googles-poodle-affects-oodles.html\', warning: \'Images/warning.png\', // feed constants evilResourceTypes: [\'cryptojacker\', \'skimmer\'], evilResourcesSalt: \'C4jhYHxW\', maxRevisions: 5, randomSaltLength: 8, // mappings of url_type => information urlTypeInformationMappings: { \'malicious_javascript/cryptojacker\': [\'web miners\', cryptojackersUrl], \'malicious_javascript/skimmer\': [\'shopping site skimmers\', skimmersUrl], }, // mappings of evilResourceTypes => user options evilResourceOptionMappings: { \'cryptojacker\': \'cryptojackers\', \'skimmer\': \'skimmers\' }, // mappings of user options => url_type optionUrlTypeMappings: { \'block\': /^(?!malicious_javascript:)/, \'cryptojackers\': /^malicious_javascript\\/cryptojacker$/, \'other-malicious-scripts\': /^malicious_javascript\\/(?!(skimmer|cryptojacker)$)/, \'skimmers\': /^malicious_javascript\\/skimmer$/, }, // report constants malwareSourceEmail: \'extension-reported-malware@netcraft.com\', malwareSourceUUID: \'mwcCcApi89cobY9loJ9rWJ9HuOhlpwMj\', extensionSourceUUID: \'qBEQ3NhEnwK4sypmuSxBJzRqpxZLRXxe\', // report error default message reportErrorMessage: \'An error occured with your submission. Please try again.\', // mapping from report API errors to displayed messages reportErrorMappings: { \'Duplicate of a recent submission\': \'This URL has already been reported.\', \'Supplied email is not valid\': \'Invalid email address. Please correct it and try again.\', }, whitelistErrorMessages: { invalidHostname: \'Invalid hostname, please correct it and try again\', duplicate: \'Protection is already disabled for this site\', }, // browserAction icon paths iconNormal: { 16: \'Images/action-normal-16.png\', 24: \'Images/action-normal-24.png\', 32: \'Images/action-normal-32.png\' }, iconDanger: { 16: \'Images/action-danger-16.png\', 24: \'Images/action-danger-24.png\', 32: \'Images/action-danger-32.png\' }, iconWarning: { 16: \'Images/action-warning-16.png\', 24: \'Images/action-warning-24.png\', 32: \'Images/action-warning-32.png\' }, iconDisabled: { 16: \'Images/action-disabled-16.png\', 24: \'Images/action-disabled-24.png\', 32: \'Images/action-disabled-32.png\' }, // Extension internal message types capturedIntercept: \'interceptDetailsCaptured\', confirmedIntercept: \'interceptDetailsConfirmed\', contentReady: \'contentScriptReady\', leaksEnabled: \'credentialLeaksEnabled\', reportURL: \'report\', reqBlock: \'block\', reqDetails: \'details\', reqForce: \'force\', reqPopUp: \'popup\', reqPrintTimingData: \'time\', //debug reqInputs: \'requestInputValues\', sendInputs: \'sendInputs\', sendIntercept: \'sendInterceptDetails\', updateOption: \'updateOption\', updateWhitelist: \'whitelist\', // disable button whitelist // Status codes errEvilResource: \'EVILRESOURCE\', errNoDetails: \'NODETAILS\', errNoInfo: \'NOINFO\', errBlocked: \'BLOCKED\', errPending: \'PENDING\', errServer: \'ERROR\', errSuspicious: \'SUSPICIOUS\', errTimeout: \'TIMEOUT\', errXSS: \'XSS\', noError: \'OK\', // Valid protocols (it is a regex match so ^ matches start of string) protocols: [\'^http\', \'^https\', \'^ftp\'], // Human readable messages msgNa: \'NA\', msgNewSite: \'New Site\', msgNoInfo: \'Site information not available\', msgNoServer: \'Unable to contact Netcraft servers\', msgSuspicious: \'Suspicious URL Detected\', msgXSS: \'Suspected XSS Attack\', msgBlocked: { \'malicious_javascript/skimmer\': { title: \'Shopping Site Skimmer Detected\', description: \'shopping site skimmer\' }, \'malicious_javascript/dropsite\': { title: \'Potential Credential Leak Detected\', description: \'potential credential leak\' }, \'malicious_javascript/cryptojacker\': { title: \'Web Miner Detected\', description: \'web miner\' }, \'malware_c2/infrastructure\': { title: \'Malicious URL\', description: \'malicious page\' }, \'default\': { title: \'Suspected Phishing\', description: \'suspected phishing site\' }, }, // Human readable reasons urlTypeCrimeMappings: { \'malicious_javascript/skimmer\': evilResourceCrime, \'malicious_javascript/cryptojacker\': evilResourceCrime, \'malicious_javascript/dropsite\': \' is blocked because we detected that sensitive information was leaked from\', }, // Timeout function value to clear cache etc. timeout: 300000, // Skimmer whitelist update interval (1 day) skimmerWhitelistInterval: 1000 * 60 * 60 * 24, // Google Analytics account account: \'UA-2150242-8\', gaCategory: \'Popup load times\', // InputAnalyser constants minInputSize: 5, // First addresses of the private IPv4 netblocks privateIPv4Blocks: [ \'10.0.0.0\', \'172.16.0.0\', \'192.168.0.0\' ], // Sync storage key for disable for this site button whitelist disableButtonWhitelistKey: \'disableButtonWhitelist\', // Local storage key for local credential leak detection whitelist localCredentialLeakWhitelistKey: \'localCredentialLeakWhitelist\', // Whitelisted fileName regexes for credential leak detection credentialLeakFileNameWhitelist: [ /^https?:\\/\\/googleads.g.doubleclick.net\\//, ], }; })(); /* * recursively prevents any property or sub-property in cons from: * - being added; * - being removed; or * - being modified. */ function deepFreeze(object) { Object.freeze(object); for (const name of Object.getOwnPropertyNames(object)) { if (typeof object[name] === \'object\') { deepFreeze(object[name]); } } } deepFreeze(cons); /* * RelevantInput objects check whether given form inputs should be * treated as sensitive data. * * This should be treated as an abstract class. Only the subclasses * are intended to be instantiated. */ class RelevantInput { /* * Enum of the possible return values of test(): * RELEVANT: * Input is relevant now, outgoing requests should be * checked for the contained value (subject to minimum length). * POTENTIALLY RELEVANT: * Input should not be considered relevant now, but may be in * future so should continue to be checked. * NOT_RELEVANT: * Input is not relevant and will not become relevant, so can * be ommitted from further checks to optimise performance. */ static get relevance() { return { NOT_RELEVANT: 1, POTENTIALLY_RELEVANT: 2, RELEVANT: 3, }; } /* * relevantInputTypes (string) - a comma separated list of * selectors that describe the types of input that should be * considered relevant. */ constructor(relevantInputTypes) { this.relevantInputTypes = relevantInputTypes; } /* * Checks whether any of the given input\'s \'labels\' (the ID, name, * or actual label) match the given regex. */ static testInputLabel(regex, input) { const labels = [ input.id, input.name, InputAnalyser.getLabelText(input), ]; return labels.some(label => regex.test(label)); } } /* * RelevantInput for passwords. */ class Password extends RelevantInput { constructor() { super(\'input[type=password]\'); } /* * Passwords are verified purely by their input type. */ test(input) { if (input.matches(this.relevantInputTypes)) { return RelevantInput.relevance.RELEVANT; } else { return RelevantInput.relevance.NOT_RELEVANT; } } } /* * RelevantInput for card numbers. */ class CardNumber extends RelevantInput { constructor() { // Inputs with no type attribute act like text inputs super( \'input[type=text], input[type=tel], input[type=number], input:not([type])\' ); // Definitely card number labels this.inputLabelRegexes = [ /(cc|card).?(num|no)/i, /&#12459;&#12540;&#12489;&#30058;&#21495;/, /&#1053;&#1086;&#1084;&#1077;&#1088;.*&#1082;&#1072;&#1088;&#1090;&#1099;/i, /&#20449;&#29992;&#21345;&#21345;&#34399;/i, /&#20449;&#29992;&#21345;&#21495;&#30721;/i, ]; /* * Matches incomplete card numbers, because ideally the * skimmer should be detected before the victim gives away * most of their card number. */ this.inputValueRegexes = [/^(\\d{4}[-\\s]?){0,4}\\d{1,4}$/]; // Probably card number labels this.suspectInputLabelRegexes = [ /card/i, /tarjeta/i, /carte/i, /&#1082;&#1072;&#1088;&#1090;&#1099;/i, /&#21345;/i, /&#12459;&#12540;&#12489;/i, /&#52852;&#46300;/i, /credit[^s]/i, ]; // If it matches one of these, probably not a card number this.inputLabelExceptionRegexes = [ /post/i, /code/i, /zip/i, /acc(oun)?t?[-_]?n(um|o)/i, /credit[-_]?(sum|limit)/i, /loyalty/i, /gift/i, /idcard/i, /cardid/i, /club/i, /sku/i, /(\\b|_)otp/i, /price/i, /amount/i, /service/i, /picard/i, /search/i, /contact/i, /callme/i, /chat/i, /activate/i, /msisdn/i, ]; } test(input) { if (!input.matches(this.relevantInputTypes)) { return RelevantInput.relevance.NOT_RELEVANT; } // Return NOT_RELEVANT if label matches an exception regex if ( this.inputLabelExceptionRegexes.some(r => RelevantInput.testInputLabel(r, input) ) ) { return RelevantInput.relevance.NOT_RELEVANT; } // Return RELEVANT if label clearly states card number if ( this.inputLabelRegexes.some(r => RelevantInput.testInputLabel(r, input) ) ) { return RelevantInput.relevance.RELEVANT; } /* * If label suggests card number, return RELEVANT if value * also suggests card number or POTENTIALLY_RELEVANT if * not. Otherwise, return NOT_RELEVANT as this element will * not become relevant unless a label changes (unlikely). */ if ( this.suspectInputLabelRegexes.some(r => RelevantInput.testInputLabel(r, input) ) ) { if (this.inputValueRegexes.some(r => r.test(input.value))) { return RelevantInput.relevance.RELEVANT; } else { return RelevantInput.relevance.POTENTIALLY_RELEVANT; } } else { return RelevantInput.relevance.NOT_RELEVANT; } } } const inputAnalyser = new InputAnalyser(); const resourceBlockList = new Set([ \"6C590F97DDB1A11AE795B3050E4C3866\", \"D285BE79786E2ECE396941331C5BA65E\", \"59DF8523520DE7F08FB53D3D12BD94F5\", \"1FB784EBAFDA1618023756BF5B6D7E46\", \"0D6E1B5F03B81F5C37813B22E29C4FE2\", \"EFAB0ECE9383D50C555B8168C3E2A4DD\", \"AC8A4B13A411A4E43E61D0A034FA4A55\", \"25A781F6D1F94EEACCF1E00E844D9E14\", \"3920B04ED0D215EA738F524F60485F23\", \"05F4A2D3161A92EB82A44F15FC2A3AEC\", \"C5A1BE7B3855B0A25275B2877E005CB9\", \"00069CEC347076721EBE2257978A0A33\", \"9A513130D02F360BBDC16D8AC0C579D8\", \"7CDB2A3A29E50F90C1000252EF523AD6\", \"25CE912FAF2376026AAD0174AD5A0329\", \"739E1F67AA5F64A06FC469F6030802D4\", \"F49368C9A05C29EF49411999C5987F2C\", \"AB2DD89FB892155275D8780CF527BC93\", \"CFE758EB18FBFE70B1F690241B75D2B1\", \"E43C3463B40A790545F4149FF06B419E\", \"8B6D4C76A7CCF5EB720660B8B2D3F5EB\", \"45573B3E2F613AF5BF2145DF004BABD1\", \"E3DB4E968B371DE94CE607199A062F39\", \"A4EFF1240C5DE614B394FB738CBD28E2\", \"F42D10E84B9A69A8A24062319F425A83\", \"90646BE39982594A58EDF5D2630CCFFD\", \"669853BEA9B84EA51D7302A7FBFF5BC3\", \"0BA566B4777526F451108100107DC5ED\", \"AB19E12BFFBC47244BDCC2403EAA9037\", \"61DAF8537240639277FD1A5908A131A4\", \"6F1DC1309CFE734C038685D8F17A3B6B\", \"DCC3DB001334F29C1C0208D15E9EAB49\", \"2DBD7D54022C28273ADE3147B8EEB076\", \"AE3760E93412C65EC5BF63537B037019\", \"4CD96A830DAC0E0B5EBE578B0DD870AE\", \"4C70569BBE1821F5655382660807FA77\", \"05ECA17FF0EA78669ED43B2BD7B63571\", \"44E503B3621513B9D3666F284E4DF47A\", \"8F1C92E7665860AE70CBB228DED86111\", \"D69552885B55C4097E5725F805815639\", \"B4357A9D889E974CDF3F9740E48F9A1B\", \"5709DC60CFC365B7064B06C709A9D32B\", \"14E8729CBB16AC3625EAA0D7A679E56E\", \"793D4DABAAA377A12F2B6CE88C5FB389\", \"C5D1DCE2AE0D8CE79054FC16CB13D65A\", \"89F91750BD4A9397D1F8CE183757FF70\", \"50B8BC213FB188A687173C1A0A77EA57\" ]); const evilJSPatterns = new Set([ ]); // Extract body object function extractBody(data) { if (!data) { return Promise.resolve({}); } if (FormData.prototype.isPrototypeOf(data)) { const formData = {}; for (const [key, value] of data.entries()) { formData[key] = value; } return Promise.resolve({ formData }); } // Create a temporary Request object to parse the body const tempRequest = new Request(\'\', { body: data, method: \'POST\' }); return extractBodyFromRequest(tempRequest); } function extractBodyFromRequest(request) { // Try to extract as text return request .text() .then(text => { return { text }; }) .catch(e => { return {}; }); } /** * This function checks if the request hostname/URL is in the blocklist. * Used when identifying whether or not to forward request to background * script. * @param hostname Hostname to check against blocklist. * @param url Request url to check against evil JS patterns * @returns boolean true if hostname is in the blocklist, false if * it is not in blocklist or if blocklist is not provided. */ function requestInBlocklist(hostname, url) { if (resourceBlockList === undefined || evilJSPatterns === undefined) { return false; } if (testEvilJS(url.replace(/^(https?:)?\\/\\//i, \'\'))) { return true; } while (!testEvilJS(hostname) && !testResource(hostname)) { const newHostname = hostname.replace(/^.+?\\./g, \'\'); if (hostname === newHostname) return false; hostname = newHostname; } return true; } function testEvilJS(url) { return evilJSPatterns.has(base64hash(sha512, url)); } function testResource(hostname) { return resourceBlockList.has( md5(cons.evilResourcesSalt + hostname).toUpperCase() ); } /* * Implementations to overwrite window.XMLHttpRequest.prototype.open, * window.XMLHttpRequest.prototype.send and window.fetch. These are (to my knowledge) the two * APIs that can be used to make requests within Javascript. If there are more, then they will * also need to be overridden and intercepted like below. */ /** * This function generates functions to be used for wrapping and intercepting network * requests, namely from XMLHttpRequest and the Fetch API. It works by taking the real * function we want to intercept the request for, and then uses document.currentScript to work * out which file initiated the request. It sends those details to the content script which * forwards it to the background script, and then we await a confirmation of receipt from the * background page. Finally, the real request is sent by forwarding parameters to the original * function passed in. * @param realFn A reference to the real function that is being intercepted. * @param getRequestUrlFn A function used to get the request URL from the object. This is * necessary as the mechanisms for different objects differs depending on implementation. * @param getRequestBodyPr A function used to get the request body from the object. Returns * a promise. It should be null if it\'s not applicable (e.g. if it sets the src of an image or script). * @param type The type of request, which will be a ResourceType: * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType * @returns {function(...[*]=): PromiseLike} */ function generateRequestInterceptFn( realFn, getRequestUrlFn, getRequestBodyPr, type ) { const patched = function(...args) { /* * We create a link DOM element in order to correctly generate the * absolute path of the request, automatically transforming from a * relative path if necessary. */ const requestUrl = normaliseUrl( getRequestUrlFn.bind(this)(...args) ); let requestHostname; let pageHostname; try { requestHostname = new URL(requestUrl).hostname; pageHostname = new URL(location.href).hostname; } catch (e) { return realFn.apply(this, args); } /* * If hostname is in the blocklist, the request should always be * sent to the background script. */ // eslint-disable-next-line prefer-const let forceStall = false; if (!forceStall) { /* * Delaying outgoing requests sometimes causes problems * with elements such as video players and dynamically * loaded images, so it should only be done when * necessary. Before inputAnalyser is defined and when the * request is to the same hostname as the page, this can * be skipped. */ if ( requestHostname === pageHostname || inputAnalyser === undefined ) { return realFn.apply(this, args); } /* * It can also be skipped if there is no sensitive * information entered into a form on the page yet. */ const relevantFilledInputs = inputAnalyser.relevantFilledInputs; if (!relevantFilledInputs.length) { return realFn.apply(this, args); } /* * For requests with no body, it\'s only necessary to delay * them when the sensitive information from the form * inputs is present in the request URL. */ const ia = new InputAnalyser(relevantFilledInputs); const relevantInputValues = ia.relevantInputValues; const urlMatcher = new URLParamsMatcher(requestUrl); if ( getRequestBodyPr === null && !urlMatcher.findInputMatches(relevantInputValues).length ) { return realFn.apply(this, args); } } /* * Create a promise that resolves immediately if the request body * does not apply to this request. */ if (getRequestBodyPr === null) { getRequestBodyPr = function() { return Promise.resolve({}); }; } let fileName; if (document.currentScript && document.currentScript.src) { fileName = document.currentScript.src; } else if (callbackCurrentScript && callbackCurrentScript.src) { fileName = callbackCurrentScript.src; } else { fileName = location.href; } return getRequestBodyPr .bind(this)(...args) .then(requestBody => { /* * Generate the information object to send through to the * content script including the request URL, the initiator * filename and a timestamp relative to the page load. */ const detail = { fileName, requestBody, requestUrl, timestamp: performance.now(), type, }; document.dispatchEvent( new CustomEvent(cons.capturedIntercept, { detail, }) ); return new Promise(resolve => { /* * Send the initiator\'s file name and other info to the * background script, then wait for confirmation that the * details made it to the background page before calling * the real function. This is to avoid any race conditions * whereby the details haven\'t been added to the tab data * before onBeforeRequest/onBeforeSendHeaders gets called. */ document.addEventListener( cons.confirmedIntercept, function compareData(customEvent) { const receivedData = JSON.parse( customEvent.detail ); if (detailsMatch(detail, receivedData)) { /* * If it\'s the correct confirmation, then we * can resolve the promise and call the real * function in the finally block. */ document.removeEventListener( cons.confirmedIntercept, compareData, false ); resolve(); } } ); }).then(() => realFn.apply(this, args)); }); }; // make toString() look native, #34 patched.toString = () => realFn.toString(); return patched; } function detailsMatch(first, second) { const comparisonKeys = [\'fileName\', \'requestUrl\', \'timestamp\', \'type\']; for (const key of comparisonKeys) { if (first[key] !== second[key]) { return false; } } return true; } window.fetch = generateRequestInterceptFn( realFetch, request => (typeof request === \'string\' ? request : request.url), (request, options) => { // Check if argument is a Request object. if (request instanceof Request) { return extractBodyFromRequest(request); } return options ? extractBody(options.body) : Promise.resolve({}); }, \'xmlhttprequest\' ); window.XMLHttpRequest.prototype.send = generateRequestInterceptFn( realSend, function getRequestUrl() { return this._requestUrl; }, extractBody, \'xmlhttprequest\' ); /* * This function is intercepted so that we can capture the value of the * request URL, since it isn\'t present in the arguments for * XMLHttpRequest.send (above). */ window.XMLHttpRequest.prototype.open = function open(...args) { [, this._requestUrl] = args; realOpen.apply(this, args); }; window.XMLHttpRequest.prototype.open.toString = () => realOpen.toString(); /* * Monkeypatches for setting the src of images and scripts, an * indirect way of sending requests that is often employed by * skimmers. The getters are also overridden to make the behaviour * seem identical to the original implementation. Otherwise, if a * script were to set an src and immediately read it, it would * come back as the old value. */ /* * WeakMap maps an object to a value. This is used to store the * new src of an object when its true src property may not yet * have been set. */ const srcMap = new WeakMap(); const requestElements = [ { srcElement: HTMLImageElement.prototype, type: \'image\', }, { srcElement: HTMLScriptElement.prototype, type: \'script\', }, ]; for (const { srcElement, type } of requestElements) { const realGetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\') .get; const realSetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\') .set; const realGetAttribute = srcElement.getAttribute; const realSetAttribute = srcElement.setAttribute; const realRemoveAttribute = srcElement.removeAttribute; const interceptFn = generateRequestInterceptFn( realSetSrc, src => `${src}`, null, type ); function setSrc(url) { /* * It\'s valid to provide a TrustedScriptURL wrapping the URL but we need to * return a string in the getter, so we convert the url to a string here. * * The url can also be undefined/null so we can\'t just call `.toString()`. * * See #56. */ srcMap.set(this, `${url}`); interceptFn.call(this, url); return url; } function getSrc() { if (srcMap.get(this) === undefined) { return realGetSrc.call(this); } else { return normaliseUrl(srcMap.get(this)); } } getSrc.toString = () => realGetSrc.toString(); setSrc.toString = () => realSetSrc.toString(); Object.defineProperty(srcElement, \'src\', { get: getSrc, set: setSrc, }); srcElement.setAttribute = function(attributeName, value) { if (attributeName.toUpperCase() === \'SRC\') { setSrc.call(this, value); } else { realSetAttribute.call(this, attributeName, value); } }; srcElement.setAttribute.toString = () => realSetAttribute.toString(); srcElement.getAttribute = function(attributeName) { if ( attributeName.toUpperCase() === \'SRC\' && srcMap.get(this) !== undefined ) { return srcMap.get(this); } else { return realGetAttribute.call(this, attributeName); } }; srcElement.getAttribute.toString = () => realGetAttribute.toString(); srcElement.removeAttribute = function(attributeName) { if (attributeName.toUpperCase() === \'SRC\') { srcMap.delete(this); } return realRemoveAttribute.call(this, attributeName); }; srcElement.removeAttribute.toString = () => realRemoveAttribute.toString(); } })( window.XMLHttpRequest.prototype.open, window.XMLHttpRequest.prototype.send, window.fetch ); /* * This entire block is wrapped in an IIFE to prevent polluting the scope of the web page with * functions created by this extension. */ (function(realOpen, realSend, realFetch) { /* * Sometimes pages overwrite standard objects with global * variables. Where possible, we extract fresh versions from an * iframe. */ const iframe = document.createElement(\'iframe\'); document.documentElement.appendChild(iframe); // Initialise with global values let URL = window.URL; let WeakMap = window.WeakMap; let WeakSet = window.WeakSet; let TextDecoder = window.TextDecoder; let Uint8Array = window.Uint8Array; let decodeURIComponent = window.decodeURIComponent; let URLSearchParams = window.URLSearchParams; let Array = window.Array; let Object = window.Object; let atob = window.atob; let CustomEvent = window.CustomEvent; let performance = window.performance; let JSON = window.JSON; /* * Under some circumstances the iframe isn\'t added * successfully. If it has a contentWindow then we can extract the * values from it, but otherwise we fall back on the global * values. */ if (iframe.contentWindow) { // Try block because sometimes the CORS prevents access to iframe try { URL = iframe.contentWindow.URL; WeakMap = iframe.contentWindow.WeakMap; WeakSet = iframe.contentWindow.WeakSet; TextDecoder = iframe.contentWindow.TextDecoder; Uint8Array = iframe.contentWindow.Uint8Array; decodeURIComponent = iframe.contentWindow.decodeURIComponent; URLSearchParams = iframe.contentWindow.URLSearchParams; Array = iframe.contentWindow.Array; Object = iframe.contentWindow.Object; atob = iframe.contentWindow.atob; CustomEvent = iframe.contentWindow.CustomEvent; performance = iframe.contentWindow.performance; JSON = iframe.contentWindow.JSON; } catch (error) { // Empty catch block to prevent errors from showing in page logs } } // If iframe was added successfully, remove it if (iframe.parentNode) { iframe.parentNode.removeChild(iframe); } /* * If any of the values don\'t have the correct type, return and * don\'t attempt any monkey patching. This avoids breaking web * pages. */ for (f of [ URL, WeakMap, WeakSet, TextDecoder, Uint8Array, decodeURIComponent, URLSearchParams, Array, Object, atob, CustomEvent, ]) { if (typeof f !== \'function\') { return; } } for (o of [performance, JSON]) { if (typeof o !== \'object\') { return; } } /* * Override common funcions that accept callbacks, such as * setInterval, to keep track of what script each callback comes * from. Skimmers commonly have the actual skimming code in a * callback, so most of the time when we intercept a request, * document.currentScript is null. To solve this, the script is * stored in a closure when the callback is passed, and assigned * to callbackCurrentscript when the callback is run. */ let callbackCurrentScript = null; // Maps callbacks originally passed to patched versions const patchedCallbackMap = new WeakMap(); // Maps patched callbacks to original versions const originalCallbackMap = new WeakMap(); /** * Generates the function with which to override the * callback-accepting functions. * @param realFn The real function, e.g. setInterval. * @param callbackIndex The index of the callback in the * arguments of realFn. For example, the callback is the first * argument in setInterval, so callbackIndex should be 0. */ function generateAddCallbackFunction(realFn, callbackIndex) { const patched = function(...args) { const script = document.currentScript || callbackCurrentScript; const callback = args[callbackIndex]; // If the callback isn\'t a function, there\'s nothing to patch if (typeof callback !== \'function\') { return realFn.apply(this, args); } // If a patch exists, use it instead of creating new function let patchedCallback = patchedCallbackMap.get(callback); if (!patchedCallback) { patchedCallback = function() { callbackCurrentScript = script; const retVal = callback.apply(this, [...arguments]); callbackCurrentScript = null; return retVal; }; patchedCallbackMap.set(callback, patchedCallback); originalCallbackMap.set(patchedCallback, callback); } args[callbackIndex] = patchedCallback; return realFn.apply(this, args); }; // make toString() look native, #34 patched.toString = () => realFn.toString(); return patched; } const realSetInterval = window.setInterval; window.setInterval = generateAddCallbackFunction(realSetInterval, 0); const realSetTimeout = window.setTimeout; window.setTimeout = generateAddCallbackFunction(realSetTimeout, 0); const realPromiseThen = Promise.prototype.then; Promise.prototype.then = generateAddCallbackFunction(realPromiseThen, 0); const realPromiseCatch = Promise.prototype.catch; Promise.prototype.catch = generateAddCallbackFunction(realPromiseCatch, 0); const realPromiseFinally = Promise.prototype.finally; if (realPromiseFinally) { Promise.prototype.finally = generateAddCallbackFunction( realPromiseFinally, 0 ); } const realAddEventListener = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = generateAddCallbackFunction( realAddEventListener, 1 ); /* * removeEventListener needs to be patched to map the passed * listener to the patched listener. */ const realRemoveEventListener = EventTarget.prototype.removeEventListener; EventTarget.prototype.removeEventListener = function(...args) { args[1] = patchedCallbackMap.get(args[1]) || args[1]; return realRemoveEventListener.apply(this, args); }; EventTarget.prototype.removeEventListener.toString = () => realRemoveEventListener.toString(); /* * Event listeners are sometimes set by directly assigning to a * property from the GlobalEventHandlers mixin, * e.g. \'submitBtn.onclick\'. For each of the GlobalEventHandlers * implementations that are likely to be relevant, the event * properties likely to be used by skimmers are overridden with * our version that tracks currentScript when assigned. */ for (const eventTarget of [ HTMLElement.prototype, Document.prototype, window, ]) { for (const eventName of [ \'onclick\', \'onblur\', \'onfocus\', \'onchange\', \'oninput\', \'onkeydown\', \'onkeypress\', \'onkeyup\', \'onload\', \'onloadend\', \'onmousedown\', \'onmouseenter\', \'onmouseleave\', \'onmousemove\', \'onmouseout\', \'onmouseover\', \'onmouseup\', \'onpointerdown\', \'onpointermove\', \'onpointerup\', \'onpointerover\', \'onpointerout\', \'onpointerenter\', \'onpointerleave\', \'onsubmit\', ]) { const descriptor = Object.getOwnPropertyDescriptor( eventTarget, eventName ); if (!descriptor) { continue; } const realGetter = descriptor.get; const realSetter = descriptor.set; const patchedSetter = generateAddCallbackFunction(realSetter, 0); /* * In case a script checks the value of the property after * setting it, the getter has to be patched to return the * original callback. */ const patchedGetter = function() { const realValue = realGetter.call(this); const originalCallback = originalCallbackMap.get(realValue); if (originalCallback) { return originalCallback; } else { return realValue; } }; patchedGetter.toString = () => realGetter.toString(); Object.defineProperty(eventTarget, eventName, { get: patchedGetter, set: patchedSetter, }); } } /* * This script has some dependencies that need to be included * without polluting the global namespace. This is achieved by * inserting them into the source at build time in the * makefile. This is preferable to loading the dependencies via * AJAX, because the latter doesn\'t work if the CSP disallows * unsafe-eval. The following tag tells the makefile where to * insert the dependencies. */ // class InputAnalyser { constructor(_inputs) { this._inputs = _inputs; this._irrelevantInputs = new WeakSet(); /* * Not currently using Password because it was causing too * many false positives. Some sites use external auth domains, * and many have misconfigured analytics that collect * passwords. */ this.relevantInputs = [new CardNumber()]; } get relevantInputValues() { const inputs = {}; this._inputs.forEach( input => (inputs[input.id || input.name] = input.value) ); return inputs; } get relevantFilledInputs() { return this.allInputs .filter(input => this.isRelevantElement(input)) .filter(input => input.value.length >= cons.minInputSize) .map(input => ({ id: input.id, name: input.name, value: input.value, })); } get allInputs() { this._inputs = [ ...document.querySelectorAll( this.relevantInputs.map(i => i.relevantInputTypes).join(\', \') ), ]; return this._inputs; } static getLabelText(input) { if (!input.labels.length) { return \'\'; } return stripSpecialChars(input.labels[0].textContent).trim(); } /** * Takes an HTMLElement and determines whether or not it could be relevant for the purposes of * skimmer detection. * @param {HTMLElement} input The element which is to be checked for relevancy. * @returns {boolean} Returns true if the element is potentially relevant (e.g. a credit card * field or other such sensitive input). */ isRelevantElement(input) { // If the element has been identified as irrelevant, return false if (this._irrelevantInputs.has(input)) { return false; } // Check the input against all the relevant input types const relevances = this.relevantInputs.map(i => i.test(input)); /* * If they all agree that this input is unlikely to become * relevant, exclude it from further checks and return false */ if (relevances.every(i => i === RelevantInput.relevance.NOT_RELEVANT)) { this._irrelevantInputs.add(input); return false; } // Return true if any consider the input to be relevant, otherwise false return relevances.some(i => i === RelevantInput.relevance.RELEVANT); } } /* * Input Matcher represents a class that can have matches with relevant inputs. * Each class that inherits must implement the findInputMatches method. */ class InputMatcher { /* * Finds any matches with the list of inputs. * @param inputs. An object whose values represent relevant inputs. * @return {[String]} The keys of the inputs that were found. */ findInputMatches(inputs) { throw new Error(\'findInputMatches has not been implemented.\'); } /** * Determines whether one or more values in needles are present in any of * the keys of the haystacks. * Checks both raw input and base64 encoded versions of the needles. * @param needles An object. The haystacks are searched for the values of * this object. * @param haystacks The array of strings in which the needle(s) may reside. * A string may optionally be supplied if only one value is present. * @returns {[String]} The keys of the needles that were found in one or * more haystacks in either raw or base64 encoded form. */ static atLeastOneNeedleInHaystack(needles, haystacks) { if ( needles === undefined || needles.length === 0 || haystacks === undefined ) { return []; } const searchArray = Array.isArray(haystacks) ? haystacks : [haystacks]; return Object.entries(needles) .filter(([_, needle]) => searchArray.some(haystack => { if (isBase64Encoded(haystack)) { if (atob(haystack).indexOf(needle) !== -1) { return true; } } return haystack.indexOf(needle) !== -1; }) ) .map(([input, _]) => input); } } /* * Matcher for an object that cannot contain any inputs. */ class EmptyMatcher extends InputMatcher { findInputMatches(inputs) { return []; } } class URLParamsMatcher extends InputMatcher { constructor(url) { super(); this._url = url; } findInputMatches(inputs) { try { const parsedUrl = new URL(this._url); return InputMatcher.atLeastOneNeedleInHaystack(inputs, [ ...parsedUrl.searchParams.values(), ]); } catch (e) { return []; } } } class StringBodyMatcher extends InputMatcher { constructor(str) { super(); this._str = str; } findInputMatches(inputs) { let decoded; try { decoded = decodeURIComponent(this._str); } catch (e) { decoded = this._str; } const searchParams = new URLSearchParams(decoded); const parsedQuery = {}; const parsedValues = []; for (const [key, value] of searchParams.entries()) { parsedQuery[key] = value; parsedValues.push(value); } if (parsedQuery !== {} && parsedValues.indexOf(null) === -1) { return InputMatcher.atLeastOneNeedleInHaystack( inputs, parsedValues ); } return InputMatcher.atLeastOneNeedleInHaystack(inputs, decoded); } } class FormBodyMatcher extends InputMatcher { constructor(formData) { super(); this._formData = formData; } findInputMatches(inputs) { return InputMatcher.atLeastOneNeedleInHaystack( inputs, Object.values(this._formData).flat() ); } } /** * Takes a string and removes a variety of \'special\' characters like punctuation. * @param str The string to be modified. * @returns {string} The resulting string without any special characters. */ function stripSpecialChars(str) { return str.replace(/[`~!@#$%^&*()_|+\\-=?;:\'\",.}{\\][\\\\/]/gi, \'\'); } /** * Attempts to identify where a string is base-64 encoded or not. * @param string The raw string to be examined. * @returns {boolean} Returns true if the string matches a base-64 string. False otherwise. */ function isBase64Encoded(string) { return /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/.test( string ); } // Computes the base64 sha512/sha224 hash of some text, removing trailing == function base64hash(hashFunction, text) { return btoa( hashFunction .array(text) .map(char => String.fromCharCode(char)) .join(\'\') ).slice(0, -2); } function normaliseUrl(str) { const link = document.createElement(\'a\'); link.href = str; return link.href; } function stripCredentials(urlstr) { try { const url = new URL(urlstr); const params = url.searchParams; params.forEach((_, key) => params.set(key, \'\')); return url.toString(); } catch (e) { return urlstr; } } /** * Check if two URLs match regardless of any partial encoding. * * Uses decodeURI which can throw a URIError when url is malformed. */ function urlsMatch(first, second) { try { return decodeURI(first) === decodeURI(second); } catch (e) { return false; } } function sendMessage(type, data, tabId, options) { if (tabId) { chrome.tabs.sendMessage(tabId, { data, type }, options); } else { chrome.runtime.sendMessage({ data, type }); } } // Encodes the given input using base64url as defined by RFC4648 section 5. function base64Url(input) { return btoa(input) .replace(\'+\', \'-\') .replace(\'/\', \'_\'); } /* * Netcraft Extension * constants JavaScript */ /* eslint sort-keys: \'off\' */ /* sort-keys is disabled as the constants are separated by type */ // Aligned values here make for easier reading, so we disable prettier // prettier-ignore const cons = (function() { const cryptojackersUrl = \'https://www.netcraft.com/apps/help/glossary/#crypto-miners\'; const skimmersUrl = \'https://www.netcraft.com/apps/help/glossary/#credit-card-skimmers\'; const evilResourceCrime = \'attempted to load\'; return { fields: [ \'topsites\', \'country\', \'netblock\', \'hoster\', \'firstseen\', \'rank\', \'risk\', \'uses_sslv3\', \'pfs\', \'heartbleed\', \'heartbleed_message\', ], // options that can be toggle in options.js and installed flag toggleOptions: [ \'installed\', \'block\', \'xss\', \'suspicious\', \'other-malicious-scripts\', \'credential-leaks\', \'analytics\', \'cryptojackers\', \'skimmers\', ], // domains mirror: \'https://mirror2.extension.netcraft.com/\', toolbar: \'https://mirror.toolbar.netcraft.com/\', // links and faqs blogArchive: \'https://news.netcraft.com/archives/\', browserApp: \'https://www.netcraft.com/apps/browser/\', cryptojackersUrl, extensionStore: \'https://addons.mozilla.org/en-us/firefox/addon/netcraft-toolbar?src=external-blocked_visit-rating\', heartbleedUrl: \'https://www.netcraft.com/apps/help/glossary/#heartbleed\', logBlockedVisit: \'https://toolbar.netcraft.com/blocked_visit\', netblock: \'https://sitereport.netcraft.com/netblock?q=\', pfsUrl: \'https://www.netcraft.com/apps/help/glossary/#pfs\', report: \'https://sitereport.netcraft.com/?url=\', reportAPI: \'https://report.netcraft.com/api/v2/report/urls\', reportMistake: \'https://report.netcraft.com/report/mistake?url=\', skimmersUrl, sslv3Url: \'https://www.netcraft.com/apps/help/glossary/#sslv3\', submissionURL: \'https://report.netcraft.com/submission/\', topsites: \'https://trends.netcraft.com/topsites?s=\', // paths block: \'blocked.html\', check: \'check_url/v4/\', danger: \'Images/danger.png\', embed: \'/embedded\', evilJSPatternsFeed: \'eviljspatterns\', skimmerDomainWhitelistFeed: \'eviljsskimmerwhitelist\', evilResourcesFeed: \'blockdb/netcraft_extension_evil_resource_encrypted-yppL7bVh/\', flags: \'Images/flags/\', force: \'/dodns\', heartbleed: \'Images/heartbleed.png\', info: \'/info\', poodlePost: \'2014/10/15/googles-poodle-affects-oodles.html\', warning: \'Images/warning.png\', // feed constants evilResourceTypes: [\'cryptojacker\', \'skimmer\'], evilResourcesSalt: \'C4jhYHxW\', maxRevisions: 5, randomSaltLength: 8, // mappings of url_type => information urlTypeInformationMappings: { \'malicious_javascript/cryptojacker\': [\'web miners\', cryptojackersUrl], \'malicious_javascript/skimmer\': [\'shopping site skimmers\', skimmersUrl], }, // mappings of evilResourceTypes => user options evilResourceOptionMappings: { \'cryptojacker\': \'cryptojackers\', \'skimmer\': \'skimmers\' }, // mappings of user options => url_type optionUrlTypeMappings: { \'block\': /^(?!malicious_javascript:)/, \'cryptojackers\': /^malicious_javascript\\/cryptojacker$/, \'other-malicious-scripts\': /^malicious_javascript\\/(?!(skimmer|cryptojacker)$)/, \'skimmers\': /^malicious_javascript\\/skimmer$/, }, // report constants malwareSourceEmail: \'extension-reported-malware@netcraft.com\', malwareSourceUUID: \'mwcCcApi89cobY9loJ9rWJ9HuOhlpwMj\', extensionSourceUUID: \'qBEQ3NhEnwK4sypmuSxBJzRqpxZLRXxe\', // report error default message reportErrorMessage: \'An error occured with your submission. Please try again.\', // mapping from report API errors to displayed messages reportErrorMappings: { \'Duplicate of a recent submission\': \'This URL has already been reported.\', \'Supplied email is not valid\': \'Invalid email address. Please correct it and try again.\', }, whitelistErrorMessages: { invalidHostname: \'Invalid hostname, please correct it and try again\', duplicate: \'Protection is already disabled for this site\', }, // browserAction icon paths iconNormal: { 16: \'Images/action-normal-16.png\', 24: \'Images/action-normal-24.png\', 32: \'Images/action-normal-32.png\' }, iconDanger: { 16: \'Images/action-danger-16.png\', 24: \'Images/action-danger-24.png\', 32: \'Images/action-danger-32.png\' }, iconWarning: { 16: \'Images/action-warning-16.png\', 24: \'Images/action-warning-24.png\', 32: \'Images/action-warning-32.png\' }, iconDisabled: { 16: \'Images/action-disabled-16.png\', 24: \'Images/action-disabled-24.png\', 32: \'Images/action-disabled-32.png\' }, // Extension internal message types capturedIntercept: \'interceptDetailsCaptured\', confirmedIntercept: \'interceptDetailsConfirmed\', contentReady: \'contentScriptReady\', leaksEnabled: \'credentialLeaksEnabled\', reportURL: \'report\', reqBlock: \'block\', reqDetails: \'details\', reqForce: \'force\', reqPopUp: \'popup\', reqPrintTimingData: \'time\', //debug reqInputs: \'requestInputValues\', sendInputs: \'sendInputs\', sendIntercept: \'sendInterceptDetails\', updateOption: \'updateOption\', updateWhitelist: \'whitelist\', // disable button whitelist // Status codes errEvilResource: \'EVILRESOURCE\', errNoDetails: \'NODETAILS\', errNoInfo: \'NOINFO\', errBlocked: \'BLOCKED\', errPending: \'PENDING\', errServer: \'ERROR\', errSuspicious: \'SUSPICIOUS\', errTimeout: \'TIMEOUT\', errXSS: \'XSS\', noError: \'OK\', // Valid protocols (it is a regex match so ^ matches start of string) protocols: [\'^http\', \'^https\', \'^ftp\'], // Human readable messages msgNa: \'NA\', msgNewSite: \'New Site\', msgNoInfo: \'Site information not available\', msgNoServer: \'Unable to contact Netcraft servers\', msgSuspicious: \'Suspicious URL Detected\', msgXSS: \'Suspected XSS Attack\', msgBlocked: { \'malicious_javascript/skimmer\': { title: \'Shopping Site Skimmer Detected\', description: \'shopping site skimmer\' }, \'malicious_javascript/dropsite\': { title: \'Potential Credential Leak Detected\', description: \'potential credential leak\' }, \'malicious_javascript/cryptojacker\': { title: \'Web Miner Detected\', description: \'web miner\' }, \'malware_c2/infrastructure\': { title: \'Malicious URL\', description: \'malicious page\' }, \'default\': { title: \'Suspected Phishing\', description: \'suspected phishing site\' }, }, // Human readable reasons urlTypeCrimeMappings: { \'malicious_javascript/skimmer\': evilResourceCrime, \'malicious_javascript/cryptojacker\': evilResourceCrime, \'malicious_javascript/dropsite\': \' is blocked because we detected that sensitive information was leaked from\', }, // Timeout function value to clear cache etc. timeout: 300000, // Skimmer whitelist update interval (1 day) skimmerWhitelistInterval: 1000 * 60 * 60 * 24, // Google Analytics account account: \'UA-2150242-8\', gaCategory: \'Popup load times\', // InputAnalyser constants minInputSize: 5, // First addresses of the private IPv4 netblocks privateIPv4Blocks: [ \'10.0.0.0\', \'172.16.0.0\', \'192.168.0.0\' ], // Sync storage key for disable for this site button whitelist disableButtonWhitelistKey: \'disableButtonWhitelist\', // Local storage key for local credential leak detection whitelist localCredentialLeakWhitelistKey: \'localCredentialLeakWhitelist\', // Whitelisted fileName regexes for credential leak detection credentialLeakFileNameWhitelist: [ /^https?:\\/\\/googleads.g.doubleclick.net\\//, ], }; })(); /* * recursively prevents any property or sub-property in cons from: * - being added; * - being removed; or * - being modified. */ function deepFreeze(object) { Object.freeze(object); for (const name of Object.getOwnPropertyNames(object)) { if (typeof object[name] === \'object\') { deepFreeze(object[name]); } } } deepFreeze(cons); /* * RelevantInput objects check whether given form inputs should be * treated as sensitive data. * * This should be treated as an abstract class. Only the subclasses * are intended to be instantiated. */ class RelevantInput { /* * Enum of the possible return values of test(): * RELEVANT: * Input is relevant now, outgoing requests should be * checked for the contained value (subject to minimum length). * POTENTIALLY RELEVANT: * Input should not be considered relevant now, but may be in * future so should continue to be checked. * NOT_RELEVANT: * Input is not relevant and will not become relevant, so can * be ommitted from further checks to optimise performance. */ static get relevance() { return { NOT_RELEVANT: 1, POTENTIALLY_RELEVANT: 2, RELEVANT: 3, }; } /* * relevantInputTypes (string) - a comma separated list of * selectors that describe the types of input that should be * considered relevant. */ constructor(relevantInputTypes) { this.relevantInputTypes = relevantInputTypes; } /* * Checks whether any of the given input\'s \'labels\' (the ID, name, * or actual label) match the given regex. */ static testInputLabel(regex, input) { const labels = [ input.id, input.name, InputAnalyser.getLabelText(input), ]; return labels.some(label => regex.test(label)); } } /* * RelevantInput for passwords. */ class Password extends RelevantInput { constructor() { super(\'input[type=password]\'); } /* * Passwords are verified purely by their input type. */ test(input) { if (input.matches(this.relevantInputTypes)) { return RelevantInput.relevance.RELEVANT; } else { return RelevantInput.relevance.NOT_RELEVANT; } } } /* * RelevantInput for card numbers. */ class CardNumber extends RelevantInput { constructor() { // Inputs with no type attribute act like text inputs super( \'input[type=text], input[type=tel], input[type=number], input:not([type])\' ); // Definitely card number labels this.inputLabelRegexes = [ /(cc|card).?(num|no)/i, /&#12459;&#12540;&#12489;&#30058;&#21495;/, /&#1053;&#1086;&#1084;&#1077;&#1088;.*&#1082;&#1072;&#1088;&#1090;&#1099;/i, /&#20449;&#29992;&#21345;&#21345;&#34399;/i, /&#20449;&#29992;&#21345;&#21495;&#30721;/i, ]; /* * Matches incomplete card numbers, because ideally the * skimmer should be detected before the victim gives away * most of their card number. */ this.inputValueRegexes = [/^(\\d{4}[-\\s]?){0,4}\\d{1,4}$/]; // Probably card number labels this.suspectInputLabelRegexes = [ /card/i, /tarjeta/i, /carte/i, /&#1082;&#1072;&#1088;&#1090;&#1099;/i, /&#21345;/i, /&#12459;&#12540;&#12489;/i, /&#52852;&#46300;/i, /credit[^s]/i, ]; // If it matches one of these, probably not a card number this.inputLabelExceptionRegexes = [ /post/i, /code/i, /zip/i, /acc(oun)?t?[-_]?n(um|o)/i, /credit[-_]?(sum|limit)/i, /loyalty/i, /gift/i, /idcard/i, /cardid/i, /club/i, /sku/i, /(\\b|_)otp/i, /price/i, /amount/i, /service/i, /picard/i, /search/i, /contact/i, /callme/i, /chat/i, /activate/i, /msisdn/i, ]; } test(input) { if (!input.matches(this.relevantInputTypes)) { return RelevantInput.relevance.NOT_RELEVANT; } // Return NOT_RELEVANT if label matches an exception regex if ( this.inputLabelExceptionRegexes.some(r => RelevantInput.testInputLabel(r, input) ) ) { return RelevantInput.relevance.NOT_RELEVANT; } // Return RELEVANT if label clearly states card number if ( this.inputLabelRegexes.some(r => RelevantInput.testInputLabel(r, input) ) ) { return RelevantInput.relevance.RELEVANT; } /* * If label suggests card number, return RELEVANT if value * also suggests card number or POTENTIALLY_RELEVANT if * not. Otherwise, return NOT_RELEVANT as this element will * not become relevant unless a label changes (unlikely). */ if ( this.suspectInputLabelRegexes.some(r => RelevantInput.testInputLabel(r, input) ) ) { if (this.inputValueRegexes.some(r => r.test(input.value))) { return RelevantInput.relevance.RELEVANT; } else { return RelevantInput.relevance.POTENTIALLY_RELEVANT; } } else { return RelevantInput.relevance.NOT_RELEVANT; } } } const inputAnalyser = new InputAnalyser(); const resourceBlockList = new Set([ \"6C590F97DDB1A11AE795B3050E4C3866\", \"D285BE79786E2ECE396941331C5BA65E\", \"59DF8523520DE7F08FB53D3D12BD94F5\", \"1FB784EBAFDA1618023756BF5B6D7E46\", \"0D6E1B5F03B81F5C37813B22E29C4FE2\", \"EFAB0ECE9383D50C555B8168C3E2A4DD\", \"AC8A4B13A411A4E43E61D0A034FA4A55\", \"25A781F6D1F94EEACCF1E00E844D9E14\", \"3920B04ED0D215EA738F524F60485F23\", \"05F4A2D3161A92EB82A44F15FC2A3AEC\", \"C5A1BE7B3855B0A25275B2877E005CB9\", \"00069CEC347076721EBE2257978A0A33\", \"9A513130D02F360BBDC16D8AC0C579D8\", \"7CDB2A3A29E50F90C1000252EF523AD6\", \"25CE912FAF2376026AAD0174AD5A0329\", \"739E1F67AA5F64A06FC469F6030802D4\", \"F49368C9A05C29EF49411999C5987F2C\", \"AB2DD89FB892155275D8780CF527BC93\", \"CFE758EB18FBFE70B1F690241B75D2B1\", \"E43C3463B40A790545F4149FF06B419E\", \"8B6D4C76A7CCF5EB720660B8B2D3F5EB\", \"45573B3E2F613AF5BF2145DF004BABD1\", \"E3DB4E968B371DE94CE607199A062F39\", \"A4EFF1240C5DE614B394FB738CBD28E2\", \"F42D10E84B9A69A8A24062319F425A83\", \"90646BE39982594A58EDF5D2630CCFFD\", \"669853BEA9B84EA51D7302A7FBFF5BC3\", \"0BA566B4777526F451108100107DC5ED\", \"AB19E12BFFBC47244BDCC2403EAA9037\", \"61DAF8537240639277FD1A5908A131A4\", \"6F1DC1309CFE734C038685D8F17A3B6B\", \"DCC3DB001334F29C1C0208D15E9EAB49\", \"2DBD7D54022C28273ADE3147B8EEB076\", \"AE3760E93412C65EC5BF63537B037019\", \"4CD96A830DAC0E0B5EBE578B0DD870AE\", \"4C70569BBE1821F5655382660807FA77\", \"05ECA17FF0EA78669ED43B2BD7B63571\", \"44E503B3621513B9D3666F284E4DF47A\", \"8F1C92E7665860AE70CBB228DED86111\", \"D69552885B55C4097E5725F805815639\", \"B4357A9D889E974CDF3F9740E48F9A1B\", \"5709DC60CFC365B7064B06C709A9D32B\", \"14E8729CBB16AC3625EAA0D7A679E56E\", \"793D4DABAAA377A12F2B6CE88C5FB389\", \"C5D1DCE2AE0D8CE79054FC16CB13D65A\", \"89F91750BD4A9397D1F8CE183757FF70\", \"50B8BC213FB188A687173C1A0A77EA57\" ]); const evilJSPatterns = new Set([ ]); // Extract body object function extractBody(data) { if (!data) { return Promise.resolve({}); } if (FormData.prototype.isPrototypeOf(data)) { const formData = {}; for (const [key, value] of data.entries()) { formData[key] = value; } return Promise.resolve({ formData }); } // Create a temporary Request object to parse the body const tempRequest = new Request(\'\', { body: data, method: \'POST\' }); return extractBodyFromRequest(tempRequest); } function extractBodyFromRequest(request) { // Try to extract as text return request .text() .then(text => { return { text }; }) .catch(e => { return {}; }); } /** * This function checks if the request hostname/URL is in the blocklist. * Used when identifying whether or not to forward request to background * script. * @param hostname Hostname to check against blocklist. * @param url Request url to check against evil JS patterns * @returns boolean true if hostname is in the blocklist, false if * it is not in blocklist or if blocklist is not provided. */ function requestInBlocklist(hostname, url) { if (resourceBlockList === undefined || evilJSPatterns === undefined) { return false; } if (testEvilJS(url.replace(/^(https?:)?\\/\\//i, \'\'))) { return true; } while (!testEvilJS(hostname) && !testResource(hostname)) { const newHostname = hostname.replace(/^.+?\\./g, \'\'); if (hostname === newHostname) return false; hostname = newHostname; } return true; } function testEvilJS(url) { return evilJSPatterns.has(base64hash(sha512, url)); } function testResource(hostname) { return resourceBlockList.has( md5(cons.evilResourcesSalt + hostname).toUpperCase() ); } /* * Implementations to overwrite window.XMLHttpRequest.prototype.open, * window.XMLHttpRequest.prototype.send and window.fetch. These are (to my knowledge) the two * APIs that can be used to make requests within Javascript. If there are more, then they will * also need to be overridden and intercepted like below. */ /** * This function generates functions to be used for wrapping and intercepting network * requests, namely from XMLHttpRequest and the Fetch API. It works by taking the real * function we want to intercept the request for, and then uses document.currentScript to work * out which file initiated the request. It sends those details to the content script which * forwards it to the background script, and then we await a confirmation of receipt from the * background page. Finally, the real request is sent by forwarding parameters to the original * function passed in. * @param realFn A reference to the real function that is being intercepted. * @param getRequestUrlFn A function used to get the request URL from the object. This is * necessary as the mechanisms for different objects differs depending on implementation. * @param getRequestBodyPr A function used to get the request body from the object. Returns * a promise. It should be null if it\'s not applicable (e.g. if it sets the src of an image or script). * @param type The type of request, which will be a ResourceType: * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType * @returns {function(...[*]=): PromiseLike} */ function generateRequestInterceptFn( realFn, getRequestUrlFn, getRequestBodyPr, type ) { const patched = function(...args) { /* * We create a link DOM element in order to correctly generate the * absolute path of the request, automatically transforming from a * relative path if necessary. */ const requestUrl = normaliseUrl( getRequestUrlFn.bind(this)(...args) ); let requestHostname; let pageHostname; try { requestHostname = new URL(requestUrl).hostname; pageHostname = new URL(location.href).hostname; } catch (e) { return realFn.apply(this, args); } /* * If hostname is in the blocklist, the request should always be * sent to the background script. */ // eslint-disable-next-line prefer-const let forceStall = false; if (!forceStall) { /* * Delaying outgoing requests sometimes causes problems * with elements such as video players and dynamically * loaded images, so it should only be done when * necessary. Before inputAnalyser is defined and when the * request is to the same hostname as the page, this can * be skipped. */ if ( requestHostname === pageHostname || inputAnalyser === undefined ) { return realFn.apply(this, args); } /* * It can also be skipped if there is no sensitive * information entered into a form on the page yet. */ const relevantFilledInputs = inputAnalyser.relevantFilledInputs; if (!relevantFilledInputs.length) { return realFn.apply(this, args); } /* * For requests with no body, it\'s only necessary to delay * them when the sensitive information from the form * inputs is present in the request URL. */ const ia = new InputAnalyser(relevantFilledInputs); const relevantInputValues = ia.relevantInputValues; const urlMatcher = new URLParamsMatcher(requestUrl); if ( getRequestBodyPr === null && !urlMatcher.findInputMatches(relevantInputValues).length ) { return realFn.apply(this, args); } } /* * Create a promise that resolves immediately if the request body * does not apply to this request. */ if (getRequestBodyPr === null) { getRequestBodyPr = function() { return Promise.resolve({}); }; } let fileName; if (document.currentScript && document.currentScript.src) { fileName = document.currentScript.src; } else if (callbackCurrentScript && callbackCurrentScript.src) { fileName = callbackCurrentScript.src; } else { fileName = location.href; } return getRequestBodyPr .bind(this)(...args) .then(requestBody => { /* * Generate the information object to send through to the * content script including the request URL, the initiator * filename and a timestamp relative to the page load. */ const detail = { fileName, requestBody, requestUrl, timestamp: performance.now(), type, }; document.dispatchEvent( new CustomEvent(cons.capturedIntercept, { detail, }) ); return new Promise(resolve => { /* * Send the initiator\'s file name and other info to the * background script, then wait for confirmation that the * details made it to the background page before calling * the real function. This is to avoid any race conditions * whereby the details haven\'t been added to the tab data * before onBeforeRequest/onBeforeSendHeaders gets called. */ document.addEventListener( cons.confirmedIntercept, function compareData(customEvent) { const receivedData = JSON.parse( customEvent.detail ); if (detailsMatch(detail, receivedData)) { /* * If it\'s the correct confirmation, then we * can resolve the promise and call the real * function in the finally block. */ document.removeEventListener( cons.confirmedIntercept, compareData, false ); resolve(); } } ); }).then(() => realFn.apply(this, args)); }); }; // make toString() look native, #34 patched.toString = () => realFn.toString(); return patched; } function detailsMatch(first, second) { const comparisonKeys = [\'fileName\', \'requestUrl\', \'timestamp\', \'type\']; for (const key of comparisonKeys) { if (first[key] !== second[key]) { return false; } } return true; } window.fetch = generateRequestInterceptFn( realFetch, request => (typeof request === \'string\' ? request : request.url), (request, options) => { // Check if argument is a Request object. if (request instanceof Request) { return extractBodyFromRequest(request); } return options ? extractBody(options.body) : Promise.resolve({}); }, \'xmlhttprequest\' ); window.XMLHttpRequest.prototype.send = generateRequestInterceptFn( realSend, function getRequestUrl() { return this._requestUrl; }, extractBody, \'xmlhttprequest\' ); /* * This function is intercepted so that we can capture the value of the * request URL, since it isn\'t present in the arguments for * XMLHttpRequest.send (above). */ window.XMLHttpRequest.prototype.open = function open(...args) { [, this._requestUrl] = args; realOpen.apply(this, args); }; window.XMLHttpRequest.prototype.open.toString = () => realOpen.toString(); /* * Monkeypatches for setting the src of images and scripts, an * indirect way of sending requests that is often employed by * skimmers. The getters are also overridden to make the behaviour * seem identical to the original implementation. Otherwise, if a * script were to set an src and immediately read it, it would * come back as the old value. */ /* * WeakMap maps an object to a value. This is used to store the * new src of an object when its true src property may not yet * have been set. */ const srcMap = new WeakMap(); const requestElements = [ { srcElement: HTMLImageElement.prototype, type: \'image\', }, { srcElement: HTMLScriptElement.prototype, type: \'script\', }, ]; for (const { srcElement, type } of requestElements) { const realGetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\') .get; const realSetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\') .set; const realGetAttribute = srcElement.getAttribute; const realSetAttribute = srcElement.setAttribute; const realRemoveAttribute = srcElement.removeAttribute; const interceptFn = generateRequestInterceptFn( realSetSrc, src => `${src}`, null, type ); function setSrc(url) { /* * It\'s valid to provide a TrustedScriptURL wrapping the URL but we need to * return a string in the getter, so we convert the url to a string here. * * The url can also be undefined/null so we can\'t just call `.toString()`. * * See #56. */ srcMap.set(this, `${url}`); interceptFn.call(this, url); return url; } function getSrc() { if (srcMap.get(this) === undefined) { return realGetSrc.call(this); } else { return normaliseUrl(srcMap.get(this)); } } getSrc.toString = () => realGetSrc.toString(); setSrc.toString = () => realSetSrc.toString(); Object.defineProperty(srcElement, \'src\', { get: getSrc, set: setSrc, }); srcElement.setAttribute = function(attributeName, value) { if (attributeName.toUpperCase() === \'SRC\') { setSrc.call(this, value); } else { realSetAttribute.call(this, attributeName, value); } }; srcElement.setAttribute.toString = () => realSetAttribute.toString(); srcElement.getAttribute = function(attributeName) { if ( attributeName.toUpperCase() === \'SRC\' && srcMap.get(this) !== undefined ) { return srcMap.get(this); } else { return realGetAttribute.call(this, attributeName); } }; srcElement.getAttribute.toString = () => realGetAttribute.toString(); srcElement.removeAttribute = function(attributeName) { if (attributeName.toUpperCase() === \'SRC\') { srcMap.delete(this); } return realRemoveAttribute.call(this, attributeName); }; srcElement.removeAttribute.toString = () => realRemoveAttribute.toString(); } })( window.XMLHttpRequest.prototype.open, window.XMLHttpRequest.prototype.send, window.fetch ); @font-face { font-family: \"MS PMincho\"; src: url(https://sys.emarketing.pe/builder/font/ms-pmincho.ttf); } @font-face { font-family: \"MS Mincho\"; src: url(https://sys.emarketing.pe/builder/font/ms-mincho.ttf); } #message-htmlpart1 div.rcmBody #v1outlook a { padding: 0; } #message-htmlpart1 div.rcmBody .v1es-button { mso-style-priority: 100!important; text-decoration: none!important; } #message-htmlpart1 div.rcmBody a[x-apple-data-detectors] { color: inherit!important; text-decoration: none!important; font-size: inherit!important; font-family: inherit!important; font-weight: inherit!important; line-height: inherit!important; } #message-htmlpart1 div.rcmBody .v1es-desk-hidden { display: none; float: left; overflow: hidden; width: 0; max-height: 0; line-height: 0; mso-hide: all; } @media only screen and (max-width:600px) {#message-htmlpart1 div.rcmBody p, #message-htmlpart1 div.rcmBody ul li, #message-htmlpart1 div.rcmBody ol li, #message-htmlpart1 div.rcmBody a { line-height: 150%!important; } #message-htmlpart1 div.rcmBody h1, #message-htmlpart1 div.rcmBody h2, #message-htmlpart1 div.rcmBody h3, #message-htmlpart1 div.rcmBody h1 a, #message-htmlpart1 div.rcmBody h2 a, #message-htmlpart1 div.rcmBody h3 a { line-height: 120%; } #message-htmlpart1 div.rcmBody h1 { font-size: 30px!important; text-align: left; } #message-htmlpart1 div.rcmBody h2 { font-size: 24px!important; text-align: left; } #message-htmlpart1 div.rcmBody h3 { font-size: 20px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-header-body h1 a, #message-htmlpart1 div.rcmBody .v1es-content-body h1 a, #message-htmlpart1 div.rcmBody .v1es-footer-body h1 a { font-size: 30px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-header-body h2 a, #message-htmlpart1 div.rcmBody .v1es-content-body h2 a, #message-htmlpart1 div.rcmBody .v1es-footer-body h2 a { font-size: 24px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-header-body h3 a, #message-htmlpart1 div.rcmBody .v1es-content-body h3 a, #message-htmlpart1 div.rcmBody .v1es-footer-body h3 a { font-size: 20px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-menu td a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-header-body p, #message-htmlpart1 div.rcmBody .v1es-header-body ul li, #message-htmlpart1 div.rcmBody .v1es-header-body ol li, #message-htmlpart1 div.rcmBody .v1es-header-body a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-content-body p, #message-htmlpart1 div.rcmBody .v1es-content-body ul li, #message-htmlpart1 div.rcmBody .v1es-content-body ol li, #message-htmlpart1 div.rcmBody .v1es-content-body a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-footer-body p, #message-htmlpart1 div.rcmBody .v1es-footer-body ul li, #message-htmlpart1 div.rcmBody .v1es-footer-body ol li, #message-htmlpart1 div.rcmBody .v1es-footer-body a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-infoblock p, #message-htmlpart1 div.rcmBody .v1es-infoblock ul li, #message-htmlpart1 div.rcmBody .v1es-infoblock ol li, #message-htmlpart1 div.rcmBody .v1es-infoblock a { font-size: 12px!important; } #message-htmlpart1 div.rcmBody *[class=\"gmail-fix\"] { display: none!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-c, #message-htmlpart1 div.rcmBody .v1es-m-txt-c h1, #message-htmlpart1 div.rcmBody .v1es-m-txt-c h2, #message-htmlpart1 div.rcmBody .v1es-m-txt-c h3 { text-align: center!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-r, #message-htmlpart1 div.rcmBody .v1es-m-txt-r h1, #message-htmlpart1 div.rcmBody .v1es-m-txt-r h2, #message-htmlpart1 div.rcmBody .v1es-m-txt-r h3 { text-align: right!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-l, #message-htmlpart1 div.rcmBody .v1es-m-txt-l h1, #message-htmlpart1 div.rcmBody .v1es-m-txt-l h2, #message-htmlpart1 div.rcmBody .v1es-m-txt-l h3 { text-align: left!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-r img, #message-htmlpart1 div.rcmBody .v1es-m-txt-c img, #message-htmlpart1 div.rcmBody .v1es-m-txt-l img { display: inline!important; } #message-htmlpart1 div.rcmBody .v1es-button-border { display: inline-block!important; } #message-htmlpart1 div.rcmBody a.v1es-button, #message-htmlpart1 div.rcmBody button.v1es-button { font-size: 18px!important; display: inline-block!important; } #message-htmlpart1 div.rcmBody .v1es-adaptive table, #message-htmlpart1 div.rcmBody .v1es-left, #message-htmlpart1 div.rcmBody .v1es-right { width: 100%!important; } #message-htmlpart1 div.rcmBody .v1es-content table, #message-htmlpart1 div.rcmBody .v1es-header table, #message-htmlpart1 div.rcmBody .v1es-footer table, #message-htmlpart1 div.rcmBody .v1es-content, #message-htmlpart1 div.rcmBody .v1es-footer, #message-htmlpart1 div.rcmBody .v1es-header { width: 100%!important; max-width: 600px!important; } #message-htmlpart1 div.rcmBody .v1es-adapt-td { display: block!important; width: 100%!important; } #message-htmlpart1 div.rcmBody .v1adapt-img { width: 100%!important; height: auto!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0 { padding: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0r { padding-right: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0l { padding-left: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0t { padding-top: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0b { padding-bottom: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20b { padding-bottom: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-mobile-hidden, #message-htmlpart1 div.rcmBody .v1es-hidden { display: none!important; } #message-htmlpart1 div.rcmBody tr.v1es-desk-hidden, #message-htmlpart1 div.rcmBody td.v1es-desk-hidden, #message-htmlpart1 div.rcmBody table.v1es-desk-hidden { width: auto!important; overflow: visible!important; float: none!important; max-height: inherit!important; line-height: inherit!important; } #message-htmlpart1 div.rcmBody tr.v1es-desk-hidden { display: table-row!important; } #message-htmlpart1 div.rcmBody table.v1es-desk-hidden { display: table!important; } #message-htmlpart1 div.rcmBody td.v1es-desk-menu-hidden { display: table-cell!important; } #message-htmlpart1 div.rcmBody .v1es-menu td { width: 1%!important; } #message-htmlpart1 div.rcmBody table.v1es-table-not-adapt, #message-htmlpart1 div.rcmBody .v1esd-block-html table { width: auto!important; } #message-htmlpart1 div.rcmBody table.v1es-social { display: inline-block!important; } #message-htmlpart1 div.rcmBody table.v1es-social td { display: inline-block!important; } #message-htmlpart1 div.rcmBody .v1es-desk-hidden { display: table-row!important; width: auto!important; overflow: visible!important; max-height: inherit!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5 { padding: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5t { padding-top: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5b { padding-bottom: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5r { padding-right: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5l { padding-left: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10 { padding: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10t { padding-top: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10b { padding-bottom: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10r { padding-right: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10l { padding-left: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15 { padding: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15t { padding-top: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15b { padding-bottom: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15r { padding-right: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15l { padding-left: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20 { padding: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20t { padding-top: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20r { padding-right: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20l { padding-left: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25 { padding: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25t { padding-top: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25b { padding-bottom: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25r { padding-right: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25l { padding-left: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30 { padding: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30t { padding-top: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30b { padding-bottom: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30r { padding-right: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30l { padding-left: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35 { padding: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35t { padding-top: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35b { padding-bottom: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35r { padding-right: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35l { padding-left: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40 { padding: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40t { padding-top: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40b { padding-bottom: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40r { padding-right: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40l { padding-left: 40px!important; } } Estás seguro si tu Sitio Web esta en un sistema actualizado?Si no lo está, eres vulnerable de ataques de Hackers o Phishing Como cliente de Hosting Peru podemos ofrecerte un diagnostico completo GRATIS de su estado.Si está actualizado ¡Te felicitaremos!Si es vulnerable, HORROR!! Además del informe te podemos enviar una propuesta de renovación segura y total a precios super competitivos.Solicita sin ningún compromiso un DIAGNOSTICO GRATIS!Llenando este sencillo formulario Solicitar diagnóstico Gratis if (!window.UI) { var UI = new rcube_mail_ui(); } $(function() { rcmail.init(); }); \";s:10:\"from_email\";s:31:\"no-responder@hostingperu.com.pe\";s:9:\"from_name\";s:12:\"Hosting Peru\";s:8:\"reply_to\";s:25:\"ventas@hostingperu.com.pe\";s:6:\"status\";s:7:\"queuing\";s:9:\"sign_dkim\";i:1;s:10:\"track_open\";i:1;s:11:\"track_click\";i:1;s:6:\"resend\";N;s:6:\"run_at\";N;s:11:\"delivery_at\";N;s:10:\"created_at\";s:19:\"2023-09-11 17:09:58\";s:10:\"updated_at\";s:19:\"2023-09-11 17:26:12\";s:15:\"template_source\";N;s:10:\"last_error\";N;s:5:\"image\";N;s:20:\"default_mail_list_id\";i:1;s:18:\"tracking_domain_id\";N;s:37:\"use_default_sending_server_from_email\";i:0;s:9:\"preheader\";N;s:11:\"running_pid\";N;s:11:\"template_id\";i:28;s:19:\"skip_failed_message\";i:1;}s:11:\"\0*\0original\";a:29:{s:2:\"id\";i:1;s:3:\"uid\";s:13:\"64ff49e657faa\";s:11:\"customer_id\";i:1;s:4:\"type\";s:7:\"regular\";s:4:\"name\";s:11:\"Sin título\";s:7:\"subject\";s:70:\"La web de tu negocio puede estar en riesgo. Descubre cómo protegerla.\";s:5:\"plain\";s:87105:\" Blank @media (min-width: 768px) { .container { max-width: 650px; } } @media (min-width: 992px) { .container { max-width: 760px; } } @media (min-width: 769px) { [data-hide-on=\"desktop\"] { display: none!important; } } @media (max-width: 600px) { [data-hide-on=\"mobile\"] { display: none!important; } } @media (max-width: 600px) { [builder-element=CellContainerElement].cell-responsive { display: block!important; } [builder-element=CellContainerElement].cell-responsive [builder-element=\"CellElement\"] { width: 100%!important; max-width: none!important; } [builder-element=CellContainerElement].cell-responsive [builder-element=\"CellElement\"], [builder-element=CellContainerElement].cell-responsive [builder-element=\"BlockElement\"] { padding-left: 0px!important; padding-right: 0px!important; } /* EmailMode */ [builder-element=CellContainerElement].cell-responsive tbody, [builder-element=CellContainerElement].cell-responsive tr, [builder-element=CellContainerElement].cell-responsive td { display: block!important; width: 100%; } [builder-element=CellContainerElement].cell-responsive [builder-element=\"CellElement\"] td { padding-left: 0px!important; padding-right: 0px!important; } } @media (min-width: 600px) { [builder-element=PricingTableElement] .col-12 { flex-basis: 0; flex-grow: 1; } } /* * This entire block is wrapped in an IIFE to prevent polluting the scope of the web page with * functions created by this extension. */ (function(realOpen, realSend, realFetch) { /* * Sometimes pages overwrite standard objects with global * variables. Where possible, we extract fresh versions from an * iframe. */ const iframe = document.createElement(\'iframe\'); document.documentElement.appendChild(iframe); // Initialise with global values let URL = window.URL; let WeakMap = window.WeakMap; let WeakSet = window.WeakSet; let TextDecoder = window.TextDecoder; let Uint8Array = window.Uint8Array; let decodeURIComponent = window.decodeURIComponent; let URLSearchParams = window.URLSearchParams; let Array = window.Array; let Object = window.Object; let atob = window.atob; let CustomEvent = window.CustomEvent; let performance = window.performance; let JSON = window.JSON; /* * Under some circumstances the iframe isn\'t added * successfully. If it has a contentWindow then we can extract the * values from it, but otherwise we fall back on the global * values. */ if (iframe.contentWindow) { // Try block because sometimes the CORS prevents access to iframe try { URL = iframe.contentWindow.URL; WeakMap = iframe.contentWindow.WeakMap; WeakSet = iframe.contentWindow.WeakSet; TextDecoder = iframe.contentWindow.TextDecoder; Uint8Array = iframe.contentWindow.Uint8Array; decodeURIComponent = iframe.contentWindow.decodeURIComponent; URLSearchParams = iframe.contentWindow.URLSearchParams; Array = iframe.contentWindow.Array; Object = iframe.contentWindow.Object; atob = iframe.contentWindow.atob; CustomEvent = iframe.contentWindow.CustomEvent; performance = iframe.contentWindow.performance; JSON = iframe.contentWindow.JSON; } catch (error) { // Empty catch block to prevent errors from showing in page logs } } // If iframe was added successfully, remove it if (iframe.parentNode) { iframe.parentNode.removeChild(iframe); } /* * If any of the values don\'t have the correct type, return and * don\'t attempt any monkey patching. This avoids breaking web * pages. */ for (f of [ URL, WeakMap, WeakSet, TextDecoder, Uint8Array, decodeURIComponent, URLSearchParams, Array, Object, atob, CustomEvent, ]) { if (typeof f !== \'function\') { return; } } for (o of [performance, JSON]) { if (typeof o !== \'object\') { return; } } /* * Override common funcions that accept callbacks, such as * setInterval, to keep track of what script each callback comes * from. Skimmers commonly have the actual skimming code in a * callback, so most of the time when we intercept a request, * document.currentScript is null. To solve this, the script is * stored in a closure when the callback is passed, and assigned * to callbackCurrentscript when the callback is run. */ let callbackCurrentScript = null; // Maps callbacks originally passed to patched versions const patchedCallbackMap = new WeakMap(); // Maps patched callbacks to original versions const originalCallbackMap = new WeakMap(); /** * Generates the function with which to override the * callback-accepting functions. * @param realFn The real function, e.g. setInterval. * @param callbackIndex The index of the callback in the * arguments of realFn. For example, the callback is the first * argument in setInterval, so callbackIndex should be 0. */ function generateAddCallbackFunction(realFn, callbackIndex) { const patched = function(...args) { const script = document.currentScript || callbackCurrentScript; const callback = args[callbackIndex]; // If the callback isn\'t a function, there\'s nothing to patch if (typeof callback !== \'function\') { return realFn.apply(this, args); } // If a patch exists, use it instead of creating new function let patchedCallback = patchedCallbackMap.get(callback); if (!patchedCallback) { patchedCallback = function() { callbackCurrentScript = script; const retVal = callback.apply(this, [...arguments]); callbackCurrentScript = null; return retVal; }; patchedCallbackMap.set(callback, patchedCallback); originalCallbackMap.set(patchedCallback, callback); } args[callbackIndex] = patchedCallback; return realFn.apply(this, args); }; // make toString() look native, #34 patched.toString = () => realFn.toString(); return patched; } const realSetInterval = window.setInterval; window.setInterval = generateAddCallbackFunction(realSetInterval, 0); const realSetTimeout = window.setTimeout; window.setTimeout = generateAddCallbackFunction(realSetTimeout, 0); const realPromiseThen = Promise.prototype.then; Promise.prototype.then = generateAddCallbackFunction(realPromiseThen, 0); const realPromiseCatch = Promise.prototype.catch; Promise.prototype.catch = generateAddCallbackFunction(realPromiseCatch, 0); const realPromiseFinally = Promise.prototype.finally; if (realPromiseFinally) { Promise.prototype.finally = generateAddCallbackFunction( realPromiseFinally, 0 ); } const realAddEventListener = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = generateAddCallbackFunction( realAddEventListener, 1 ); /* * removeEventListener needs to be patched to map the passed * listener to the patched listener. */ const realRemoveEventListener = EventTarget.prototype.removeEventListener; EventTarget.prototype.removeEventListener = function(...args) { args[1] = patchedCallbackMap.get(args[1]) || args[1]; return realRemoveEventListener.apply(this, args); }; EventTarget.prototype.removeEventListener.toString = () => realRemoveEventListener.toString(); /* * Event listeners are sometimes set by directly assigning to a * property from the GlobalEventHandlers mixin, * e.g. \'submitBtn.onclick\'. For each of the GlobalEventHandlers * implementations that are likely to be relevant, the event * properties likely to be used by skimmers are overridden with * our version that tracks currentScript when assigned. */ for (const eventTarget of [ HTMLElement.prototype, Document.prototype, window, ]) { for (const eventName of [ \'onclick\', \'onblur\', \'onfocus\', \'onchange\', \'oninput\', \'onkeydown\', \'onkeypress\', \'onkeyup\', \'onload\', \'onloadend\', \'onmousedown\', \'onmouseenter\', \'onmouseleave\', \'onmousemove\', \'onmouseout\', \'onmouseover\', \'onmouseup\', \'onpointerdown\', \'onpointermove\', \'onpointerup\', \'onpointerover\', \'onpointerout\', \'onpointerenter\', \'onpointerleave\', \'onsubmit\', ]) { const descriptor = Object.getOwnPropertyDescriptor( eventTarget, eventName ); if (!descriptor) { continue; } const realGetter = descriptor.get; const realSetter = descriptor.set; const patchedSetter = generateAddCallbackFunction(realSetter, 0); /* * In case a script checks the value of the property after * setting it, the getter has to be patched to return the * original callback. */ const patchedGetter = function() { const realValue = realGetter.call(this); const originalCallback = originalCallbackMap.get(realValue); if (originalCallback) { return originalCallback; } else { return realValue; } }; patchedGetter.toString = () => realGetter.toString(); Object.defineProperty(eventTarget, eventName, { get: patchedGetter, set: patchedSetter, }); } } /* * This script has some dependencies that need to be included * without polluting the global namespace. This is achieved by * inserting them into the source at build time in the * makefile. This is preferable to loading the dependencies via * AJAX, because the latter doesn\'t work if the CSP disallows * unsafe-eval. The following tag tells the makefile where to * insert the dependencies. */ // class InputAnalyser { constructor(_inputs) { this._inputs = _inputs; this._irrelevantInputs = new WeakSet(); /* * Not currently using Password because it was causing too * many false positives. Some sites use external auth domains, * and many have misconfigured analytics that collect * passwords. */ this.relevantInputs = [new CardNumber()]; } get relevantInputValues() { const inputs = {}; this._inputs.forEach( input => (inputs[input.id || input.name] = input.value) ); return inputs; } get relevantFilledInputs() { return this.allInputs .filter(input => this.isRelevantElement(input)) .filter(input => input.value.length >= cons.minInputSize) .map(input => ({ id: input.id, name: input.name, value: input.value, })); } get allInputs() { this._inputs = [ ...document.querySelectorAll( this.relevantInputs.map(i => i.relevantInputTypes).join(\', \') ), ]; return this._inputs; } static getLabelText(input) { if (!input.labels.length) { return \'\'; } return stripSpecialChars(input.labels[0].textContent).trim(); } /** * Takes an HTMLElement and determines whether or not it could be relevant for the purposes of * skimmer detection. * @param {HTMLElement} input The element which is to be checked for relevancy. * @returns {boolean} Returns true if the element is potentially relevant (e.g. a credit card * field or other such sensitive input). */ isRelevantElement(input) { // If the element has been identified as irrelevant, return false if (this._irrelevantInputs.has(input)) { return false; } // Check the input against all the relevant input types const relevances = this.relevantInputs.map(i => i.test(input)); /* * If they all agree that this input is unlikely to become * relevant, exclude it from further checks and return false */ if (relevances.every(i => i === RelevantInput.relevance.NOT_RELEVANT)) { this._irrelevantInputs.add(input); return false; } // Return true if any consider the input to be relevant, otherwise false return relevances.some(i => i === RelevantInput.relevance.RELEVANT); } } /* * Input Matcher represents a class that can have matches with relevant inputs. * Each class that inherits must implement the findInputMatches method. */ class InputMatcher { /* * Finds any matches with the list of inputs. * @param inputs. An object whose values represent relevant inputs. * @return {[String]} The keys of the inputs that were found. */ findInputMatches(inputs) { throw new Error(\'findInputMatches has not been implemented.\'); } /** * Determines whether one or more values in needles are present in any of * the keys of the haystacks. * Checks both raw input and base64 encoded versions of the needles. * @param needles An object. The haystacks are searched for the values of * this object. * @param haystacks The array of strings in which the needle(s) may reside. * A string may optionally be supplied if only one value is present. * @returns {[String]} The keys of the needles that were found in one or * more haystacks in either raw or base64 encoded form. */ static atLeastOneNeedleInHaystack(needles, haystacks) { if ( needles === undefined || needles.length === 0 || haystacks === undefined ) { return []; } const searchArray = Array.isArray(haystacks) ? haystacks : [haystacks]; return Object.entries(needles) .filter(([_, needle]) => searchArray.some(haystack => { if (isBase64Encoded(haystack)) { if (atob(haystack).indexOf(needle) !== -1) { return true; } } return haystack.indexOf(needle) !== -1; }) ) .map(([input, _]) => input); } } /* * Matcher for an object that cannot contain any inputs. */ class EmptyMatcher extends InputMatcher { findInputMatches(inputs) { return []; } } class URLParamsMatcher extends InputMatcher { constructor(url) { super(); this._url = url; } findInputMatches(inputs) { try { const parsedUrl = new URL(this._url); return InputMatcher.atLeastOneNeedleInHaystack(inputs, [ ...parsedUrl.searchParams.values(), ]); } catch (e) { return []; } } } class StringBodyMatcher extends InputMatcher { constructor(str) { super(); this._str = str; } findInputMatches(inputs) { let decoded; try { decoded = decodeURIComponent(this._str); } catch (e) { decoded = this._str; } const searchParams = new URLSearchParams(decoded); const parsedQuery = {}; const parsedValues = []; for (const [key, value] of searchParams.entries()) { parsedQuery[key] = value; parsedValues.push(value); } if (parsedQuery !== {} && parsedValues.indexOf(null) === -1) { return InputMatcher.atLeastOneNeedleInHaystack( inputs, parsedValues ); } return InputMatcher.atLeastOneNeedleInHaystack(inputs, decoded); } } class FormBodyMatcher extends InputMatcher { constructor(formData) { super(); this._formData = formData; } findInputMatches(inputs) { return InputMatcher.atLeastOneNeedleInHaystack( inputs, Object.values(this._formData).flat() ); } } /** * Takes a string and removes a variety of \'special\' characters like punctuation. * @param str The string to be modified. * @returns {string} The resulting string without any special characters. */ function stripSpecialChars(str) { return str.replace(/[`~!@#$%^&*()_|+\\-=?;:\'\",.}{\\][\\\\/]/gi, \'\'); } /** * Attempts to identify where a string is base-64 encoded or not. * @param string The raw string to be examined. * @returns {boolean} Returns true if the string matches a base-64 string. False otherwise. */ function isBase64Encoded(string) { return /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/.test( string ); } // Computes the base64 sha512/sha224 hash of some text, removing trailing == function base64hash(hashFunction, text) { return btoa( hashFunction .array(text) .map(char => String.fromCharCode(char)) .join(\'\') ).slice(0, -2); } function normaliseUrl(str) { const link = document.createElement(\'a\'); link.href = str; return link.href; } function stripCredentials(urlstr) { try { const url = new URL(urlstr); const params = url.searchParams; params.forEach((_, key) => params.set(key, \'\')); return url.toString(); } catch (e) { return urlstr; } } /** * Check if two URLs match regardless of any partial encoding. * * Uses decodeURI which can throw a URIError when url is malformed. */ function urlsMatch(first, second) { try { return decodeURI(first) === decodeURI(second); } catch (e) { return false; } } function sendMessage(type, data, tabId, options) { if (tabId) { chrome.tabs.sendMessage(tabId, { data, type }, options); } else { chrome.runtime.sendMessage({ data, type }); } } // Encodes the given input using base64url as defined by RFC4648 section 5. function base64Url(input) { return btoa(input) .replace(\'+\', \'-\') .replace(\'/\', \'_\'); } /* * Netcraft Extension * constants JavaScript */ /* eslint sort-keys: \'off\' */ /* sort-keys is disabled as the constants are separated by type */ // Aligned values here make for easier reading, so we disable prettier // prettier-ignore const cons = (function() { const cryptojackersUrl = \'https://www.netcraft.com/apps/help/glossary/#crypto-miners\'; const skimmersUrl = \'https://www.netcraft.com/apps/help/glossary/#credit-card-skimmers\'; const evilResourceCrime = \'attempted to load\'; return { fields: [ \'topsites\', \'country\', \'netblock\', \'hoster\', \'firstseen\', \'rank\', \'risk\', \'uses_sslv3\', \'pfs\', \'heartbleed\', \'heartbleed_message\', ], // options that can be toggle in options.js and installed flag toggleOptions: [ \'installed\', \'block\', \'xss\', \'suspicious\', \'other-malicious-scripts\', \'credential-leaks\', \'analytics\', \'cryptojackers\', \'skimmers\', ], // domains mirror: \'https://mirror2.extension.netcraft.com/\', toolbar: \'https://mirror.toolbar.netcraft.com/\', // links and faqs blogArchive: \'https://news.netcraft.com/archives/\', browserApp: \'https://www.netcraft.com/apps/browser/\', cryptojackersUrl, extensionStore: \'https://addons.mozilla.org/en-us/firefox/addon/netcraft-toolbar?src=external-blocked_visit-rating\', heartbleedUrl: \'https://www.netcraft.com/apps/help/glossary/#heartbleed\', logBlockedVisit: \'https://toolbar.netcraft.com/blocked_visit\', netblock: \'https://sitereport.netcraft.com/netblock?q=\', pfsUrl: \'https://www.netcraft.com/apps/help/glossary/#pfs\', report: \'https://sitereport.netcraft.com/?url=\', reportAPI: \'https://report.netcraft.com/api/v2/report/urls\', reportMistake: \'https://report.netcraft.com/report/mistake?url=\', skimmersUrl, sslv3Url: \'https://www.netcraft.com/apps/help/glossary/#sslv3\', submissionURL: \'https://report.netcraft.com/submission/\', topsites: \'https://trends.netcraft.com/topsites?s=\', // paths block: \'blocked.html\', check: \'check_url/v4/\', danger: \'Images/danger.png\', embed: \'/embedded\', evilJSPatternsFeed: \'eviljspatterns\', skimmerDomainWhitelistFeed: \'eviljsskimmerwhitelist\', evilResourcesFeed: \'blockdb/netcraft_extension_evil_resource_encrypted-yppL7bVh/\', flags: \'Images/flags/\', force: \'/dodns\', heartbleed: \'Images/heartbleed.png\', info: \'/info\', poodlePost: \'2014/10/15/googles-poodle-affects-oodles.html\', warning: \'Images/warning.png\', // feed constants evilResourceTypes: [\'cryptojacker\', \'skimmer\'], evilResourcesSalt: \'C4jhYHxW\', maxRevisions: 5, randomSaltLength: 8, // mappings of url_type => information urlTypeInformationMappings: { \'malicious_javascript/cryptojacker\': [\'web miners\', cryptojackersUrl], \'malicious_javascript/skimmer\': [\'shopping site skimmers\', skimmersUrl], }, // mappings of evilResourceTypes => user options evilResourceOptionMappings: { \'cryptojacker\': \'cryptojackers\', \'skimmer\': \'skimmers\' }, // mappings of user options => url_type optionUrlTypeMappings: { \'block\': /^(?!malicious_javascript:)/, \'cryptojackers\': /^malicious_javascript\\/cryptojacker$/, \'other-malicious-scripts\': /^malicious_javascript\\/(?!(skimmer|cryptojacker)$)/, \'skimmers\': /^malicious_javascript\\/skimmer$/, }, // report constants malwareSourceEmail: \'extension-reported-malware@netcraft.com\', malwareSourceUUID: \'mwcCcApi89cobY9loJ9rWJ9HuOhlpwMj\', extensionSourceUUID: \'qBEQ3NhEnwK4sypmuSxBJzRqpxZLRXxe\', // report error default message reportErrorMessage: \'An error occured with your submission. Please try again.\', // mapping from report API errors to displayed messages reportErrorMappings: { \'Duplicate of a recent submission\': \'This URL has already been reported.\', \'Supplied email is not valid\': \'Invalid email address. Please correct it and try again.\', }, whitelistErrorMessages: { invalidHostname: \'Invalid hostname, please correct it and try again\', duplicate: \'Protection is already disabled for this site\', }, // browserAction icon paths iconNormal: { 16: \'Images/action-normal-16.png\', 24: \'Images/action-normal-24.png\', 32: \'Images/action-normal-32.png\' }, iconDanger: { 16: \'Images/action-danger-16.png\', 24: \'Images/action-danger-24.png\', 32: \'Images/action-danger-32.png\' }, iconWarning: { 16: \'Images/action-warning-16.png\', 24: \'Images/action-warning-24.png\', 32: \'Images/action-warning-32.png\' }, iconDisabled: { 16: \'Images/action-disabled-16.png\', 24: \'Images/action-disabled-24.png\', 32: \'Images/action-disabled-32.png\' }, // Extension internal message types capturedIntercept: \'interceptDetailsCaptured\', confirmedIntercept: \'interceptDetailsConfirmed\', contentReady: \'contentScriptReady\', leaksEnabled: \'credentialLeaksEnabled\', reportURL: \'report\', reqBlock: \'block\', reqDetails: \'details\', reqForce: \'force\', reqPopUp: \'popup\', reqPrintTimingData: \'time\', //debug reqInputs: \'requestInputValues\', sendInputs: \'sendInputs\', sendIntercept: \'sendInterceptDetails\', updateOption: \'updateOption\', updateWhitelist: \'whitelist\', // disable button whitelist // Status codes errEvilResource: \'EVILRESOURCE\', errNoDetails: \'NODETAILS\', errNoInfo: \'NOINFO\', errBlocked: \'BLOCKED\', errPending: \'PENDING\', errServer: \'ERROR\', errSuspicious: \'SUSPICIOUS\', errTimeout: \'TIMEOUT\', errXSS: \'XSS\', noError: \'OK\', // Valid protocols (it is a regex match so ^ matches start of string) protocols: [\'^http\', \'^https\', \'^ftp\'], // Human readable messages msgNa: \'NA\', msgNewSite: \'New Site\', msgNoInfo: \'Site information not available\', msgNoServer: \'Unable to contact Netcraft servers\', msgSuspicious: \'Suspicious URL Detected\', msgXSS: \'Suspected XSS Attack\', msgBlocked: { \'malicious_javascript/skimmer\': { title: \'Shopping Site Skimmer Detected\', description: \'shopping site skimmer\' }, \'malicious_javascript/dropsite\': { title: \'Potential Credential Leak Detected\', description: \'potential credential leak\' }, \'malicious_javascript/cryptojacker\': { title: \'Web Miner Detected\', description: \'web miner\' }, \'malware_c2/infrastructure\': { title: \'Malicious URL\', description: \'malicious page\' }, \'default\': { title: \'Suspected Phishing\', description: \'suspected phishing site\' }, }, // Human readable reasons urlTypeCrimeMappings: { \'malicious_javascript/skimmer\': evilResourceCrime, \'malicious_javascript/cryptojacker\': evilResourceCrime, \'malicious_javascript/dropsite\': \' is blocked because we detected that sensitive information was leaked from\', }, // Timeout function value to clear cache etc. timeout: 300000, // Skimmer whitelist update interval (1 day) skimmerWhitelistInterval: 1000 * 60 * 60 * 24, // Google Analytics account account: \'UA-2150242-8\', gaCategory: \'Popup load times\', // InputAnalyser constants minInputSize: 5, // First addresses of the private IPv4 netblocks privateIPv4Blocks: [ \'10.0.0.0\', \'172.16.0.0\', \'192.168.0.0\' ], // Sync storage key for disable for this site button whitelist disableButtonWhitelistKey: \'disableButtonWhitelist\', // Local storage key for local credential leak detection whitelist localCredentialLeakWhitelistKey: \'localCredentialLeakWhitelist\', // Whitelisted fileName regexes for credential leak detection credentialLeakFileNameWhitelist: [ /^https?:\\/\\/googleads.g.doubleclick.net\\//, ], }; })(); /* * recursively prevents any property or sub-property in cons from: * - being added; * - being removed; or * - being modified. */ function deepFreeze(object) { Object.freeze(object); for (const name of Object.getOwnPropertyNames(object)) { if (typeof object[name] === \'object\') { deepFreeze(object[name]); } } } deepFreeze(cons); /* * RelevantInput objects check whether given form inputs should be * treated as sensitive data. * * This should be treated as an abstract class. Only the subclasses * are intended to be instantiated. */ class RelevantInput { /* * Enum of the possible return values of test(): * RELEVANT: * Input is relevant now, outgoing requests should be * checked for the contained value (subject to minimum length). * POTENTIALLY RELEVANT: * Input should not be considered relevant now, but may be in * future so should continue to be checked. * NOT_RELEVANT: * Input is not relevant and will not become relevant, so can * be ommitted from further checks to optimise performance. */ static get relevance() { return { NOT_RELEVANT: 1, POTENTIALLY_RELEVANT: 2, RELEVANT: 3, }; } /* * relevantInputTypes (string) - a comma separated list of * selectors that describe the types of input that should be * considered relevant. */ constructor(relevantInputTypes) { this.relevantInputTypes = relevantInputTypes; } /* * Checks whether any of the given input\'s \'labels\' (the ID, name, * or actual label) match the given regex. */ static testInputLabel(regex, input) { const labels = [ input.id, input.name, InputAnalyser.getLabelText(input), ]; return labels.some(label => regex.test(label)); } } /* * RelevantInput for passwords. */ class Password extends RelevantInput { constructor() { super(\'input[type=password]\'); } /* * Passwords are verified purely by their input type. */ test(input) { if (input.matches(this.relevantInputTypes)) { return RelevantInput.relevance.RELEVANT; } else { return RelevantInput.relevance.NOT_RELEVANT; } } } /* * RelevantInput for card numbers. */ class CardNumber extends RelevantInput { constructor() { // Inputs with no type attribute act like text inputs super( \'input[type=text], input[type=tel], input[type=number], input:not([type])\' ); // Definitely card number labels this.inputLabelRegexes = [ /(cc|card).?(num|no)/i, /&#12459;&#12540;&#12489;&#30058;&#21495;/, /&#1053;&#1086;&#1084;&#1077;&#1088;.*&#1082;&#1072;&#1088;&#1090;&#1099;/i, /&#20449;&#29992;&#21345;&#21345;&#34399;/i, /&#20449;&#29992;&#21345;&#21495;&#30721;/i, ]; /* * Matches incomplete card numbers, because ideally the * skimmer should be detected before the victim gives away * most of their card number. */ this.inputValueRegexes = [/^(\\d{4}[-\\s]?){0,4}\\d{1,4}$/]; // Probably card number labels this.suspectInputLabelRegexes = [ /card/i, /tarjeta/i, /carte/i, /&#1082;&#1072;&#1088;&#1090;&#1099;/i, /&#21345;/i, /&#12459;&#12540;&#12489;/i, /&#52852;&#46300;/i, /credit[^s]/i, ]; // If it matches one of these, probably not a card number this.inputLabelExceptionRegexes = [ /post/i, /code/i, /zip/i, /acc(oun)?t?[-_]?n(um|o)/i, /credit[-_]?(sum|limit)/i, /loyalty/i, /gift/i, /idcard/i, /cardid/i, /club/i, /sku/i, /(\\b|_)otp/i, /price/i, /amount/i, /service/i, /picard/i, /search/i, /contact/i, /callme/i, /chat/i, /activate/i, /msisdn/i, ]; } test(input) { if (!input.matches(this.relevantInputTypes)) { return RelevantInput.relevance.NOT_RELEVANT; } // Return NOT_RELEVANT if label matches an exception regex if ( this.inputLabelExceptionRegexes.some(r => RelevantInput.testInputLabel(r, input) ) ) { return RelevantInput.relevance.NOT_RELEVANT; } // Return RELEVANT if label clearly states card number if ( this.inputLabelRegexes.some(r => RelevantInput.testInputLabel(r, input) ) ) { return RelevantInput.relevance.RELEVANT; } /* * If label suggests card number, return RELEVANT if value * also suggests card number or POTENTIALLY_RELEVANT if * not. Otherwise, return NOT_RELEVANT as this element will * not become relevant unless a label changes (unlikely). */ if ( this.suspectInputLabelRegexes.some(r => RelevantInput.testInputLabel(r, input) ) ) { if (this.inputValueRegexes.some(r => r.test(input.value))) { return RelevantInput.relevance.RELEVANT; } else { return RelevantInput.relevance.POTENTIALLY_RELEVANT; } } else { return RelevantInput.relevance.NOT_RELEVANT; } } } const inputAnalyser = new InputAnalyser(); const resourceBlockList = new Set([ \"6C590F97DDB1A11AE795B3050E4C3866\", \"D285BE79786E2ECE396941331C5BA65E\", \"59DF8523520DE7F08FB53D3D12BD94F5\", \"1FB784EBAFDA1618023756BF5B6D7E46\", \"0D6E1B5F03B81F5C37813B22E29C4FE2\", \"EFAB0ECE9383D50C555B8168C3E2A4DD\", \"AC8A4B13A411A4E43E61D0A034FA4A55\", \"25A781F6D1F94EEACCF1E00E844D9E14\", \"3920B04ED0D215EA738F524F60485F23\", \"05F4A2D3161A92EB82A44F15FC2A3AEC\", \"C5A1BE7B3855B0A25275B2877E005CB9\", \"00069CEC347076721EBE2257978A0A33\", \"9A513130D02F360BBDC16D8AC0C579D8\", \"7CDB2A3A29E50F90C1000252EF523AD6\", \"25CE912FAF2376026AAD0174AD5A0329\", \"739E1F67AA5F64A06FC469F6030802D4\", \"F49368C9A05C29EF49411999C5987F2C\", \"AB2DD89FB892155275D8780CF527BC93\", \"CFE758EB18FBFE70B1F690241B75D2B1\", \"E43C3463B40A790545F4149FF06B419E\", \"8B6D4C76A7CCF5EB720660B8B2D3F5EB\", \"45573B3E2F613AF5BF2145DF004BABD1\", \"E3DB4E968B371DE94CE607199A062F39\", \"A4EFF1240C5DE614B394FB738CBD28E2\", \"F42D10E84B9A69A8A24062319F425A83\", \"90646BE39982594A58EDF5D2630CCFFD\", \"669853BEA9B84EA51D7302A7FBFF5BC3\", \"0BA566B4777526F451108100107DC5ED\", \"AB19E12BFFBC47244BDCC2403EAA9037\", \"61DAF8537240639277FD1A5908A131A4\", \"6F1DC1309CFE734C038685D8F17A3B6B\", \"DCC3DB001334F29C1C0208D15E9EAB49\", \"2DBD7D54022C28273ADE3147B8EEB076\", \"AE3760E93412C65EC5BF63537B037019\", \"4CD96A830DAC0E0B5EBE578B0DD870AE\", \"4C70569BBE1821F5655382660807FA77\", \"05ECA17FF0EA78669ED43B2BD7B63571\", \"44E503B3621513B9D3666F284E4DF47A\", \"8F1C92E7665860AE70CBB228DED86111\", \"D69552885B55C4097E5725F805815639\", \"B4357A9D889E974CDF3F9740E48F9A1B\", \"5709DC60CFC365B7064B06C709A9D32B\", \"14E8729CBB16AC3625EAA0D7A679E56E\", \"793D4DABAAA377A12F2B6CE88C5FB389\", \"C5D1DCE2AE0D8CE79054FC16CB13D65A\", \"89F91750BD4A9397D1F8CE183757FF70\", \"50B8BC213FB188A687173C1A0A77EA57\" ]); const evilJSPatterns = new Set([ ]); // Extract body object function extractBody(data) { if (!data) { return Promise.resolve({}); } if (FormData.prototype.isPrototypeOf(data)) { const formData = {}; for (const [key, value] of data.entries()) { formData[key] = value; } return Promise.resolve({ formData }); } // Create a temporary Request object to parse the body const tempRequest = new Request(\'\', { body: data, method: \'POST\' }); return extractBodyFromRequest(tempRequest); } function extractBodyFromRequest(request) { // Try to extract as text return request .text() .then(text => { return { text }; }) .catch(e => { return {}; }); } /** * This function checks if the request hostname/URL is in the blocklist. * Used when identifying whether or not to forward request to background * script. * @param hostname Hostname to check against blocklist. * @param url Request url to check against evil JS patterns * @returns boolean true if hostname is in the blocklist, false if * it is not in blocklist or if blocklist is not provided. */ function requestInBlocklist(hostname, url) { if (resourceBlockList === undefined || evilJSPatterns === undefined) { return false; } if (testEvilJS(url.replace(/^(https?:)?\\/\\//i, \'\'))) { return true; } while (!testEvilJS(hostname) && !testResource(hostname)) { const newHostname = hostname.replace(/^.+?\\./g, \'\'); if (hostname === newHostname) return false; hostname = newHostname; } return true; } function testEvilJS(url) { return evilJSPatterns.has(base64hash(sha512, url)); } function testResource(hostname) { return resourceBlockList.has( md5(cons.evilResourcesSalt + hostname).toUpperCase() ); } /* * Implementations to overwrite window.XMLHttpRequest.prototype.open, * window.XMLHttpRequest.prototype.send and window.fetch. These are (to my knowledge) the two * APIs that can be used to make requests within Javascript. If there are more, then they will * also need to be overridden and intercepted like below. */ /** * This function generates functions to be used for wrapping and intercepting network * requests, namely from XMLHttpRequest and the Fetch API. It works by taking the real * function we want to intercept the request for, and then uses document.currentScript to work * out which file initiated the request. It sends those details to the content script which * forwards it to the background script, and then we await a confirmation of receipt from the * background page. Finally, the real request is sent by forwarding parameters to the original * function passed in. * @param realFn A reference to the real function that is being intercepted. * @param getRequestUrlFn A function used to get the request URL from the object. This is * necessary as the mechanisms for different objects differs depending on implementation. * @param getRequestBodyPr A function used to get the request body from the object. Returns * a promise. It should be null if it\'s not applicable (e.g. if it sets the src of an image or script). * @param type The type of request, which will be a ResourceType: * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType * @returns {function(...[*]=): PromiseLike} */ function generateRequestInterceptFn( realFn, getRequestUrlFn, getRequestBodyPr, type ) { const patched = function(...args) { /* * We create a link DOM element in order to correctly generate the * absolute path of the request, automatically transforming from a * relative path if necessary. */ const requestUrl = normaliseUrl( getRequestUrlFn.bind(this)(...args) ); let requestHostname; let pageHostname; try { requestHostname = new URL(requestUrl).hostname; pageHostname = new URL(location.href).hostname; } catch (e) { return realFn.apply(this, args); } /* * If hostname is in the blocklist, the request should always be * sent to the background script. */ // eslint-disable-next-line prefer-const let forceStall = false; if (!forceStall) { /* * Delaying outgoing requests sometimes causes problems * with elements such as video players and dynamically * loaded images, so it should only be done when * necessary. Before inputAnalyser is defined and when the * request is to the same hostname as the page, this can * be skipped. */ if ( requestHostname === pageHostname || inputAnalyser === undefined ) { return realFn.apply(this, args); } /* * It can also be skipped if there is no sensitive * information entered into a form on the page yet. */ const relevantFilledInputs = inputAnalyser.relevantFilledInputs; if (!relevantFilledInputs.length) { return realFn.apply(this, args); } /* * For requests with no body, it\'s only necessary to delay * them when the sensitive information from the form * inputs is present in the request URL. */ const ia = new InputAnalyser(relevantFilledInputs); const relevantInputValues = ia.relevantInputValues; const urlMatcher = new URLParamsMatcher(requestUrl); if ( getRequestBodyPr === null && !urlMatcher.findInputMatches(relevantInputValues).length ) { return realFn.apply(this, args); } } /* * Create a promise that resolves immediately if the request body * does not apply to this request. */ if (getRequestBodyPr === null) { getRequestBodyPr = function() { return Promise.resolve({}); }; } let fileName; if (document.currentScript && document.currentScript.src) { fileName = document.currentScript.src; } else if (callbackCurrentScript && callbackCurrentScript.src) { fileName = callbackCurrentScript.src; } else { fileName = location.href; } return getRequestBodyPr .bind(this)(...args) .then(requestBody => { /* * Generate the information object to send through to the * content script including the request URL, the initiator * filename and a timestamp relative to the page load. */ const detail = { fileName, requestBody, requestUrl, timestamp: performance.now(), type, }; document.dispatchEvent( new CustomEvent(cons.capturedIntercept, { detail, }) ); return new Promise(resolve => { /* * Send the initiator\'s file name and other info to the * background script, then wait for confirmation that the * details made it to the background page before calling * the real function. This is to avoid any race conditions * whereby the details haven\'t been added to the tab data * before onBeforeRequest/onBeforeSendHeaders gets called. */ document.addEventListener( cons.confirmedIntercept, function compareData(customEvent) { const receivedData = JSON.parse( customEvent.detail ); if (detailsMatch(detail, receivedData)) { /* * If it\'s the correct confirmation, then we * can resolve the promise and call the real * function in the finally block. */ document.removeEventListener( cons.confirmedIntercept, compareData, false ); resolve(); } } ); }).then(() => realFn.apply(this, args)); }); }; // make toString() look native, #34 patched.toString = () => realFn.toString(); return patched; } function detailsMatch(first, second) { const comparisonKeys = [\'fileName\', \'requestUrl\', \'timestamp\', \'type\']; for (const key of comparisonKeys) { if (first[key] !== second[key]) { return false; } } return true; } window.fetch = generateRequestInterceptFn( realFetch, request => (typeof request === \'string\' ? request : request.url), (request, options) => { // Check if argument is a Request object. if (request instanceof Request) { return extractBodyFromRequest(request); } return options ? extractBody(options.body) : Promise.resolve({}); }, \'xmlhttprequest\' ); window.XMLHttpRequest.prototype.send = generateRequestInterceptFn( realSend, function getRequestUrl() { return this._requestUrl; }, extractBody, \'xmlhttprequest\' ); /* * This function is intercepted so that we can capture the value of the * request URL, since it isn\'t present in the arguments for * XMLHttpRequest.send (above). */ window.XMLHttpRequest.prototype.open = function open(...args) { [, this._requestUrl] = args; realOpen.apply(this, args); }; window.XMLHttpRequest.prototype.open.toString = () => realOpen.toString(); /* * Monkeypatches for setting the src of images and scripts, an * indirect way of sending requests that is often employed by * skimmers. The getters are also overridden to make the behaviour * seem identical to the original implementation. Otherwise, if a * script were to set an src and immediately read it, it would * come back as the old value. */ /* * WeakMap maps an object to a value. This is used to store the * new src of an object when its true src property may not yet * have been set. */ const srcMap = new WeakMap(); const requestElements = [ { srcElement: HTMLImageElement.prototype, type: \'image\', }, { srcElement: HTMLScriptElement.prototype, type: \'script\', }, ]; for (const { srcElement, type } of requestElements) { const realGetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\') .get; const realSetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\') .set; const realGetAttribute = srcElement.getAttribute; const realSetAttribute = srcElement.setAttribute; const realRemoveAttribute = srcElement.removeAttribute; const interceptFn = generateRequestInterceptFn( realSetSrc, src => `${src}`, null, type ); function setSrc(url) { /* * It\'s valid to provide a TrustedScriptURL wrapping the URL but we need to * return a string in the getter, so we convert the url to a string here. * * The url can also be undefined/null so we can\'t just call `.toString()`. * * See #56. */ srcMap.set(this, `${url}`); interceptFn.call(this, url); return url; } function getSrc() { if (srcMap.get(this) === undefined) { return realGetSrc.call(this); } else { return normaliseUrl(srcMap.get(this)); } } getSrc.toString = () => realGetSrc.toString(); setSrc.toString = () => realSetSrc.toString(); Object.defineProperty(srcElement, \'src\', { get: getSrc, set: setSrc, }); srcElement.setAttribute = function(attributeName, value) { if (attributeName.toUpperCase() === \'SRC\') { setSrc.call(this, value); } else { realSetAttribute.call(this, attributeName, value); } }; srcElement.setAttribute.toString = () => realSetAttribute.toString(); srcElement.getAttribute = function(attributeName) { if ( attributeName.toUpperCase() === \'SRC\' && srcMap.get(this) !== undefined ) { return srcMap.get(this); } else { return realGetAttribute.call(this, attributeName); } }; srcElement.getAttribute.toString = () => realGetAttribute.toString(); srcElement.removeAttribute = function(attributeName) { if (attributeName.toUpperCase() === \'SRC\') { srcMap.delete(this); } return realRemoveAttribute.call(this, attributeName); }; srcElement.removeAttribute.toString = () => realRemoveAttribute.toString(); } })( window.XMLHttpRequest.prototype.open, window.XMLHttpRequest.prototype.send, window.fetch ); /* * This entire block is wrapped in an IIFE to prevent polluting the scope of the web page with * functions created by this extension. */ (function(realOpen, realSend, realFetch) { /* * Sometimes pages overwrite standard objects with global * variables. Where possible, we extract fresh versions from an * iframe. */ const iframe = document.createElement(\'iframe\'); document.documentElement.appendChild(iframe); // Initialise with global values let URL = window.URL; let WeakMap = window.WeakMap; let WeakSet = window.WeakSet; let TextDecoder = window.TextDecoder; let Uint8Array = window.Uint8Array; let decodeURIComponent = window.decodeURIComponent; let URLSearchParams = window.URLSearchParams; let Array = window.Array; let Object = window.Object; let atob = window.atob; let CustomEvent = window.CustomEvent; let performance = window.performance; let JSON = window.JSON; /* * Under some circumstances the iframe isn\'t added * successfully. If it has a contentWindow then we can extract the * values from it, but otherwise we fall back on the global * values. */ if (iframe.contentWindow) { // Try block because sometimes the CORS prevents access to iframe try { URL = iframe.contentWindow.URL; WeakMap = iframe.contentWindow.WeakMap; WeakSet = iframe.contentWindow.WeakSet; TextDecoder = iframe.contentWindow.TextDecoder; Uint8Array = iframe.contentWindow.Uint8Array; decodeURIComponent = iframe.contentWindow.decodeURIComponent; URLSearchParams = iframe.contentWindow.URLSearchParams; Array = iframe.contentWindow.Array; Object = iframe.contentWindow.Object; atob = iframe.contentWindow.atob; CustomEvent = iframe.contentWindow.CustomEvent; performance = iframe.contentWindow.performance; JSON = iframe.contentWindow.JSON; } catch (error) { // Empty catch block to prevent errors from showing in page logs } } // If iframe was added successfully, remove it if (iframe.parentNode) { iframe.parentNode.removeChild(iframe); } /* * If any of the values don\'t have the correct type, return and * don\'t attempt any monkey patching. This avoids breaking web * pages. */ for (f of [ URL, WeakMap, WeakSet, TextDecoder, Uint8Array, decodeURIComponent, URLSearchParams, Array, Object, atob, CustomEvent, ]) { if (typeof f !== \'function\') { return; } } for (o of [performance, JSON]) { if (typeof o !== \'object\') { return; } } /* * Override common funcions that accept callbacks, such as * setInterval, to keep track of what script each callback comes * from. Skimmers commonly have the actual skimming code in a * callback, so most of the time when we intercept a request, * document.currentScript is null. To solve this, the script is * stored in a closure when the callback is passed, and assigned * to callbackCurrentscript when the callback is run. */ let callbackCurrentScript = null; // Maps callbacks originally passed to patched versions const patchedCallbackMap = new WeakMap(); // Maps patched callbacks to original versions const originalCallbackMap = new WeakMap(); /** * Generates the function with which to override the * callback-accepting functions. * @param realFn The real function, e.g. setInterval. * @param callbackIndex The index of the callback in the * arguments of realFn. For example, the callback is the first * argument in setInterval, so callbackIndex should be 0. */ function generateAddCallbackFunction(realFn, callbackIndex) { const patched = function(...args) { const script = document.currentScript || callbackCurrentScript; const callback = args[callbackIndex]; // If the callback isn\'t a function, there\'s nothing to patch if (typeof callback !== \'function\') { return realFn.apply(this, args); } // If a patch exists, use it instead of creating new function let patchedCallback = patchedCallbackMap.get(callback); if (!patchedCallback) { patchedCallback = function() { callbackCurrentScript = script; const retVal = callback.apply(this, [...arguments]); callbackCurrentScript = null; return retVal; }; patchedCallbackMap.set(callback, patchedCallback); originalCallbackMap.set(patchedCallback, callback); } args[callbackIndex] = patchedCallback; return realFn.apply(this, args); }; // make toString() look native, #34 patched.toString = () => realFn.toString(); return patched; } const realSetInterval = window.setInterval; window.setInterval = generateAddCallbackFunction(realSetInterval, 0); const realSetTimeout = window.setTimeout; window.setTimeout = generateAddCallbackFunction(realSetTimeout, 0); const realPromiseThen = Promise.prototype.then; Promise.prototype.then = generateAddCallbackFunction(realPromiseThen, 0); const realPromiseCatch = Promise.prototype.catch; Promise.prototype.catch = generateAddCallbackFunction(realPromiseCatch, 0); const realPromiseFinally = Promise.prototype.finally; if (realPromiseFinally) { Promise.prototype.finally = generateAddCallbackFunction( realPromiseFinally, 0 ); } const realAddEventListener = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = generateAddCallbackFunction( realAddEventListener, 1 ); /* * removeEventListener needs to be patched to map the passed * listener to the patched listener. */ const realRemoveEventListener = EventTarget.prototype.removeEventListener; EventTarget.prototype.removeEventListener = function(...args) { args[1] = patchedCallbackMap.get(args[1]) || args[1]; return realRemoveEventListener.apply(this, args); }; EventTarget.prototype.removeEventListener.toString = () => realRemoveEventListener.toString(); /* * Event listeners are sometimes set by directly assigning to a * property from the GlobalEventHandlers mixin, * e.g. \'submitBtn.onclick\'. For each of the GlobalEventHandlers * implementations that are likely to be relevant, the event * properties likely to be used by skimmers are overridden with * our version that tracks currentScript when assigned. */ for (const eventTarget of [ HTMLElement.prototype, Document.prototype, window, ]) { for (const eventName of [ \'onclick\', \'onblur\', \'onfocus\', \'onchange\', \'oninput\', \'onkeydown\', \'onkeypress\', \'onkeyup\', \'onload\', \'onloadend\', \'onmousedown\', \'onmouseenter\', \'onmouseleave\', \'onmousemove\', \'onmouseout\', \'onmouseover\', \'onmouseup\', \'onpointerdown\', \'onpointermove\', \'onpointerup\', \'onpointerover\', \'onpointerout\', \'onpointerenter\', \'onpointerleave\', \'onsubmit\', ]) { const descriptor = Object.getOwnPropertyDescriptor( eventTarget, eventName ); if (!descriptor) { continue; } const realGetter = descriptor.get; const realSetter = descriptor.set; const patchedSetter = generateAddCallbackFunction(realSetter, 0); /* * In case a script checks the value of the property after * setting it, the getter has to be patched to return the * original callback. */ const patchedGetter = function() { const realValue = realGetter.call(this); const originalCallback = originalCallbackMap.get(realValue); if (originalCallback) { return originalCallback; } else { return realValue; } }; patchedGetter.toString = () => realGetter.toString(); Object.defineProperty(eventTarget, eventName, { get: patchedGetter, set: patchedSetter, }); } } /* * This script has some dependencies that need to be included * without polluting the global namespace. This is achieved by * inserting them into the source at build time in the * makefile. This is preferable to loading the dependencies via * AJAX, because the latter doesn\'t work if the CSP disallows * unsafe-eval. The following tag tells the makefile where to * insert the dependencies. */ // class InputAnalyser { constructor(_inputs) { this._inputs = _inputs; this._irrelevantInputs = new WeakSet(); /* * Not currently using Password because it was causing too * many false positives. Some sites use external auth domains, * and many have misconfigured analytics that collect * passwords. */ this.relevantInputs = [new CardNumber()]; } get relevantInputValues() { const inputs = {}; this._inputs.forEach( input => (inputs[input.id || input.name] = input.value) ); return inputs; } get relevantFilledInputs() { return this.allInputs .filter(input => this.isRelevantElement(input)) .filter(input => input.value.length >= cons.minInputSize) .map(input => ({ id: input.id, name: input.name, value: input.value, })); } get allInputs() { this._inputs = [ ...document.querySelectorAll( this.relevantInputs.map(i => i.relevantInputTypes).join(\', \') ), ]; return this._inputs; } static getLabelText(input) { if (!input.labels.length) { return \'\'; } return stripSpecialChars(input.labels[0].textContent).trim(); } /** * Takes an HTMLElement and determines whether or not it could be relevant for the purposes of * skimmer detection. * @param {HTMLElement} input The element which is to be checked for relevancy. * @returns {boolean} Returns true if the element is potentially relevant (e.g. a credit card * field or other such sensitive input). */ isRelevantElement(input) { // If the element has been identified as irrelevant, return false if (this._irrelevantInputs.has(input)) { return false; } // Check the input against all the relevant input types const relevances = this.relevantInputs.map(i => i.test(input)); /* * If they all agree that this input is unlikely to become * relevant, exclude it from further checks and return false */ if (relevances.every(i => i === RelevantInput.relevance.NOT_RELEVANT)) { this._irrelevantInputs.add(input); return false; } // Return true if any consider the input to be relevant, otherwise false return relevances.some(i => i === RelevantInput.relevance.RELEVANT); } } /* * Input Matcher represents a class that can have matches with relevant inputs. * Each class that inherits must implement the findInputMatches method. */ class InputMatcher { /* * Finds any matches with the list of inputs. * @param inputs. An object whose values represent relevant inputs. * @return {[String]} The keys of the inputs that were found. */ findInputMatches(inputs) { throw new Error(\'findInputMatches has not been implemented.\'); } /** * Determines whether one or more values in needles are present in any of * the keys of the haystacks. * Checks both raw input and base64 encoded versions of the needles. * @param needles An object. The haystacks are searched for the values of * this object. * @param haystacks The array of strings in which the needle(s) may reside. * A string may optionally be supplied if only one value is present. * @returns {[String]} The keys of the needles that were found in one or * more haystacks in either raw or base64 encoded form. */ static atLeastOneNeedleInHaystack(needles, haystacks) { if ( needles === undefined || needles.length === 0 || haystacks === undefined ) { return []; } const searchArray = Array.isArray(haystacks) ? haystacks : [haystacks]; return Object.entries(needles) .filter(([_, needle]) => searchArray.some(haystack => { if (isBase64Encoded(haystack)) { if (atob(haystack).indexOf(needle) !== -1) { return true; } } return haystack.indexOf(needle) !== -1; }) ) .map(([input, _]) => input); } } /* * Matcher for an object that cannot contain any inputs. */ class EmptyMatcher extends InputMatcher { findInputMatches(inputs) { return []; } } class URLParamsMatcher extends InputMatcher { constructor(url) { super(); this._url = url; } findInputMatches(inputs) { try { const parsedUrl = new URL(this._url); return InputMatcher.atLeastOneNeedleInHaystack(inputs, [ ...parsedUrl.searchParams.values(), ]); } catch (e) { return []; } } } class StringBodyMatcher extends InputMatcher { constructor(str) { super(); this._str = str; } findInputMatches(inputs) { let decoded; try { decoded = decodeURIComponent(this._str); } catch (e) { decoded = this._str; } const searchParams = new URLSearchParams(decoded); const parsedQuery = {}; const parsedValues = []; for (const [key, value] of searchParams.entries()) { parsedQuery[key] = value; parsedValues.push(value); } if (parsedQuery !== {} && parsedValues.indexOf(null) === -1) { return InputMatcher.atLeastOneNeedleInHaystack( inputs, parsedValues ); } return InputMatcher.atLeastOneNeedleInHaystack(inputs, decoded); } } class FormBodyMatcher extends InputMatcher { constructor(formData) { super(); this._formData = formData; } findInputMatches(inputs) { return InputMatcher.atLeastOneNeedleInHaystack( inputs, Object.values(this._formData).flat() ); } } /** * Takes a string and removes a variety of \'special\' characters like punctuation. * @param str The string to be modified. * @returns {string} The resulting string without any special characters. */ function stripSpecialChars(str) { return str.replace(/[`~!@#$%^&*()_|+\\-=?;:\'\",.}{\\][\\\\/]/gi, \'\'); } /** * Attempts to identify where a string is base-64 encoded or not. * @param string The raw string to be examined. * @returns {boolean} Returns true if the string matches a base-64 string. False otherwise. */ function isBase64Encoded(string) { return /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/.test( string ); } // Computes the base64 sha512/sha224 hash of some text, removing trailing == function base64hash(hashFunction, text) { return btoa( hashFunction .array(text) .map(char => String.fromCharCode(char)) .join(\'\') ).slice(0, -2); } function normaliseUrl(str) { const link = document.createElement(\'a\'); link.href = str; return link.href; } function stripCredentials(urlstr) { try { const url = new URL(urlstr); const params = url.searchParams; params.forEach((_, key) => params.set(key, \'\')); return url.toString(); } catch (e) { return urlstr; } } /** * Check if two URLs match regardless of any partial encoding. * * Uses decodeURI which can throw a URIError when url is malformed. */ function urlsMatch(first, second) { try { return decodeURI(first) === decodeURI(second); } catch (e) { return false; } } function sendMessage(type, data, tabId, options) { if (tabId) { chrome.tabs.sendMessage(tabId, { data, type }, options); } else { chrome.runtime.sendMessage({ data, type }); } } // Encodes the given input using base64url as defined by RFC4648 section 5. function base64Url(input) { return btoa(input) .replace(\'+\', \'-\') .replace(\'/\', \'_\'); } /* * Netcraft Extension * constants JavaScript */ /* eslint sort-keys: \'off\' */ /* sort-keys is disabled as the constants are separated by type */ // Aligned values here make for easier reading, so we disable prettier // prettier-ignore const cons = (function() { const cryptojackersUrl = \'https://www.netcraft.com/apps/help/glossary/#crypto-miners\'; const skimmersUrl = \'https://www.netcraft.com/apps/help/glossary/#credit-card-skimmers\'; const evilResourceCrime = \'attempted to load\'; return { fields: [ \'topsites\', \'country\', \'netblock\', \'hoster\', \'firstseen\', \'rank\', \'risk\', \'uses_sslv3\', \'pfs\', \'heartbleed\', \'heartbleed_message\', ], // options that can be toggle in options.js and installed flag toggleOptions: [ \'installed\', \'block\', \'xss\', \'suspicious\', \'other-malicious-scripts\', \'credential-leaks\', \'analytics\', \'cryptojackers\', \'skimmers\', ], // domains mirror: \'https://mirror2.extension.netcraft.com/\', toolbar: \'https://mirror.toolbar.netcraft.com/\', // links and faqs blogArchive: \'https://news.netcraft.com/archives/\', browserApp: \'https://www.netcraft.com/apps/browser/\', cryptojackersUrl, extensionStore: \'https://addons.mozilla.org/en-us/firefox/addon/netcraft-toolbar?src=external-blocked_visit-rating\', heartbleedUrl: \'https://www.netcraft.com/apps/help/glossary/#heartbleed\', logBlockedVisit: \'https://toolbar.netcraft.com/blocked_visit\', netblock: \'https://sitereport.netcraft.com/netblock?q=\', pfsUrl: \'https://www.netcraft.com/apps/help/glossary/#pfs\', report: \'https://sitereport.netcraft.com/?url=\', reportAPI: \'https://report.netcraft.com/api/v2/report/urls\', reportMistake: \'https://report.netcraft.com/report/mistake?url=\', skimmersUrl, sslv3Url: \'https://www.netcraft.com/apps/help/glossary/#sslv3\', submissionURL: \'https://report.netcraft.com/submission/\', topsites: \'https://trends.netcraft.com/topsites?s=\', // paths block: \'blocked.html\', check: \'check_url/v4/\', danger: \'Images/danger.png\', embed: \'/embedded\', evilJSPatternsFeed: \'eviljspatterns\', skimmerDomainWhitelistFeed: \'eviljsskimmerwhitelist\', evilResourcesFeed: \'blockdb/netcraft_extension_evil_resource_encrypted-yppL7bVh/\', flags: \'Images/flags/\', force: \'/dodns\', heartbleed: \'Images/heartbleed.png\', info: \'/info\', poodlePost: \'2014/10/15/googles-poodle-affects-oodles.html\', warning: \'Images/warning.png\', // feed constants evilResourceTypes: [\'cryptojacker\', \'skimmer\'], evilResourcesSalt: \'C4jhYHxW\', maxRevisions: 5, randomSaltLength: 8, // mappings of url_type => information urlTypeInformationMappings: { \'malicious_javascript/cryptojacker\': [\'web miners\', cryptojackersUrl], \'malicious_javascript/skimmer\': [\'shopping site skimmers\', skimmersUrl], }, // mappings of evilResourceTypes => user options evilResourceOptionMappings: { \'cryptojacker\': \'cryptojackers\', \'skimmer\': \'skimmers\' }, // mappings of user options => url_type optionUrlTypeMappings: { \'block\': /^(?!malicious_javascript:)/, \'cryptojackers\': /^malicious_javascript\\/cryptojacker$/, \'other-malicious-scripts\': /^malicious_javascript\\/(?!(skimmer|cryptojacker)$)/, \'skimmers\': /^malicious_javascript\\/skimmer$/, }, // report constants malwareSourceEmail: \'extension-reported-malware@netcraft.com\', malwareSourceUUID: \'mwcCcApi89cobY9loJ9rWJ9HuOhlpwMj\', extensionSourceUUID: \'qBEQ3NhEnwK4sypmuSxBJzRqpxZLRXxe\', // report error default message reportErrorMessage: \'An error occured with your submission. Please try again.\', // mapping from report API errors to displayed messages reportErrorMappings: { \'Duplicate of a recent submission\': \'This URL has already been reported.\', \'Supplied email is not valid\': \'Invalid email address. Please correct it and try again.\', }, whitelistErrorMessages: { invalidHostname: \'Invalid hostname, please correct it and try again\', duplicate: \'Protection is already disabled for this site\', }, // browserAction icon paths iconNormal: { 16: \'Images/action-normal-16.png\', 24: \'Images/action-normal-24.png\', 32: \'Images/action-normal-32.png\' }, iconDanger: { 16: \'Images/action-danger-16.png\', 24: \'Images/action-danger-24.png\', 32: \'Images/action-danger-32.png\' }, iconWarning: { 16: \'Images/action-warning-16.png\', 24: \'Images/action-warning-24.png\', 32: \'Images/action-warning-32.png\' }, iconDisabled: { 16: \'Images/action-disabled-16.png\', 24: \'Images/action-disabled-24.png\', 32: \'Images/action-disabled-32.png\' }, // Extension internal message types capturedIntercept: \'interceptDetailsCaptured\', confirmedIntercept: \'interceptDetailsConfirmed\', contentReady: \'contentScriptReady\', leaksEnabled: \'credentialLeaksEnabled\', reportURL: \'report\', reqBlock: \'block\', reqDetails: \'details\', reqForce: \'force\', reqPopUp: \'popup\', reqPrintTimingData: \'time\', //debug reqInputs: \'requestInputValues\', sendInputs: \'sendInputs\', sendIntercept: \'sendInterceptDetails\', updateOption: \'updateOption\', updateWhitelist: \'whitelist\', // disable button whitelist // Status codes errEvilResource: \'EVILRESOURCE\', errNoDetails: \'NODETAILS\', errNoInfo: \'NOINFO\', errBlocked: \'BLOCKED\', errPending: \'PENDING\', errServer: \'ERROR\', errSuspicious: \'SUSPICIOUS\', errTimeout: \'TIMEOUT\', errXSS: \'XSS\', noError: \'OK\', // Valid protocols (it is a regex match so ^ matches start of string) protocols: [\'^http\', \'^https\', \'^ftp\'], // Human readable messages msgNa: \'NA\', msgNewSite: \'New Site\', msgNoInfo: \'Site information not available\', msgNoServer: \'Unable to contact Netcraft servers\', msgSuspicious: \'Suspicious URL Detected\', msgXSS: \'Suspected XSS Attack\', msgBlocked: { \'malicious_javascript/skimmer\': { title: \'Shopping Site Skimmer Detected\', description: \'shopping site skimmer\' }, \'malicious_javascript/dropsite\': { title: \'Potential Credential Leak Detected\', description: \'potential credential leak\' }, \'malicious_javascript/cryptojacker\': { title: \'Web Miner Detected\', description: \'web miner\' }, \'malware_c2/infrastructure\': { title: \'Malicious URL\', description: \'malicious page\' }, \'default\': { title: \'Suspected Phishing\', description: \'suspected phishing site\' }, }, // Human readable reasons urlTypeCrimeMappings: { \'malicious_javascript/skimmer\': evilResourceCrime, \'malicious_javascript/cryptojacker\': evilResourceCrime, \'malicious_javascript/dropsite\': \' is blocked because we detected that sensitive information was leaked from\', }, // Timeout function value to clear cache etc. timeout: 300000, // Skimmer whitelist update interval (1 day) skimmerWhitelistInterval: 1000 * 60 * 60 * 24, // Google Analytics account account: \'UA-2150242-8\', gaCategory: \'Popup load times\', // InputAnalyser constants minInputSize: 5, // First addresses of the private IPv4 netblocks privateIPv4Blocks: [ \'10.0.0.0\', \'172.16.0.0\', \'192.168.0.0\' ], // Sync storage key for disable for this site button whitelist disableButtonWhitelistKey: \'disableButtonWhitelist\', // Local storage key for local credential leak detection whitelist localCredentialLeakWhitelistKey: \'localCredentialLeakWhitelist\', // Whitelisted fileName regexes for credential leak detection credentialLeakFileNameWhitelist: [ /^https?:\\/\\/googleads.g.doubleclick.net\\//, ], }; })(); /* * recursively prevents any property or sub-property in cons from: * - being added; * - being removed; or * - being modified. */ function deepFreeze(object) { Object.freeze(object); for (const name of Object.getOwnPropertyNames(object)) { if (typeof object[name] === \'object\') { deepFreeze(object[name]); } } } deepFreeze(cons); /* * RelevantInput objects check whether given form inputs should be * treated as sensitive data. * * This should be treated as an abstract class. Only the subclasses * are intended to be instantiated. */ class RelevantInput { /* * Enum of the possible return values of test(): * RELEVANT: * Input is relevant now, outgoing requests should be * checked for the contained value (subject to minimum length). * POTENTIALLY RELEVANT: * Input should not be considered relevant now, but may be in * future so should continue to be checked. * NOT_RELEVANT: * Input is not relevant and will not become relevant, so can * be ommitted from further checks to optimise performance. */ static get relevance() { return { NOT_RELEVANT: 1, POTENTIALLY_RELEVANT: 2, RELEVANT: 3, }; } /* * relevantInputTypes (string) - a comma separated list of * selectors that describe the types of input that should be * considered relevant. */ constructor(relevantInputTypes) { this.relevantInputTypes = relevantInputTypes; } /* * Checks whether any of the given input\'s \'labels\' (the ID, name, * or actual label) match the given regex. */ static testInputLabel(regex, input) { const labels = [ input.id, input.name, InputAnalyser.getLabelText(input), ]; return labels.some(label => regex.test(label)); } } /* * RelevantInput for passwords. */ class Password extends RelevantInput { constructor() { super(\'input[type=password]\'); } /* * Passwords are verified purely by their input type. */ test(input) { if (input.matches(this.relevantInputTypes)) { return RelevantInput.relevance.RELEVANT; } else { return RelevantInput.relevance.NOT_RELEVANT; } } } /* * RelevantInput for card numbers. */ class CardNumber extends RelevantInput { constructor() { // Inputs with no type attribute act like text inputs super( \'input[type=text], input[type=tel], input[type=number], input:not([type])\' ); // Definitely card number labels this.inputLabelRegexes = [ /(cc|card).?(num|no)/i, /&#12459;&#12540;&#12489;&#30058;&#21495;/, /&#1053;&#1086;&#1084;&#1077;&#1088;.*&#1082;&#1072;&#1088;&#1090;&#1099;/i, /&#20449;&#29992;&#21345;&#21345;&#34399;/i, /&#20449;&#29992;&#21345;&#21495;&#30721;/i, ]; /* * Matches incomplete card numbers, because ideally the * skimmer should be detected before the victim gives away * most of their card number. */ this.inputValueRegexes = [/^(\\d{4}[-\\s]?){0,4}\\d{1,4}$/]; // Probably card number labels this.suspectInputLabelRegexes = [ /card/i, /tarjeta/i, /carte/i, /&#1082;&#1072;&#1088;&#1090;&#1099;/i, /&#21345;/i, /&#12459;&#12540;&#12489;/i, /&#52852;&#46300;/i, /credit[^s]/i, ]; // If it matches one of these, probably not a card number this.inputLabelExceptionRegexes = [ /post/i, /code/i, /zip/i, /acc(oun)?t?[-_]?n(um|o)/i, /credit[-_]?(sum|limit)/i, /loyalty/i, /gift/i, /idcard/i, /cardid/i, /club/i, /sku/i, /(\\b|_)otp/i, /price/i, /amount/i, /service/i, /picard/i, /search/i, /contact/i, /callme/i, /chat/i, /activate/i, /msisdn/i, ]; } test(input) { if (!input.matches(this.relevantInputTypes)) { return RelevantInput.relevance.NOT_RELEVANT; } // Return NOT_RELEVANT if label matches an exception regex if ( this.inputLabelExceptionRegexes.some(r => RelevantInput.testInputLabel(r, input) ) ) { return RelevantInput.relevance.NOT_RELEVANT; } // Return RELEVANT if label clearly states card number if ( this.inputLabelRegexes.some(r => RelevantInput.testInputLabel(r, input) ) ) { return RelevantInput.relevance.RELEVANT; } /* * If label suggests card number, return RELEVANT if value * also suggests card number or POTENTIALLY_RELEVANT if * not. Otherwise, return NOT_RELEVANT as this element will * not become relevant unless a label changes (unlikely). */ if ( this.suspectInputLabelRegexes.some(r => RelevantInput.testInputLabel(r, input) ) ) { if (this.inputValueRegexes.some(r => r.test(input.value))) { return RelevantInput.relevance.RELEVANT; } else { return RelevantInput.relevance.POTENTIALLY_RELEVANT; } } else { return RelevantInput.relevance.NOT_RELEVANT; } } } const inputAnalyser = new InputAnalyser(); const resourceBlockList = new Set([ \"6C590F97DDB1A11AE795B3050E4C3866\", \"D285BE79786E2ECE396941331C5BA65E\", \"59DF8523520DE7F08FB53D3D12BD94F5\", \"1FB784EBAFDA1618023756BF5B6D7E46\", \"0D6E1B5F03B81F5C37813B22E29C4FE2\", \"EFAB0ECE9383D50C555B8168C3E2A4DD\", \"AC8A4B13A411A4E43E61D0A034FA4A55\", \"25A781F6D1F94EEACCF1E00E844D9E14\", \"3920B04ED0D215EA738F524F60485F23\", \"05F4A2D3161A92EB82A44F15FC2A3AEC\", \"C5A1BE7B3855B0A25275B2877E005CB9\", \"00069CEC347076721EBE2257978A0A33\", \"9A513130D02F360BBDC16D8AC0C579D8\", \"7CDB2A3A29E50F90C1000252EF523AD6\", \"25CE912FAF2376026AAD0174AD5A0329\", \"739E1F67AA5F64A06FC469F6030802D4\", \"F49368C9A05C29EF49411999C5987F2C\", \"AB2DD89FB892155275D8780CF527BC93\", \"CFE758EB18FBFE70B1F690241B75D2B1\", \"E43C3463B40A790545F4149FF06B419E\", \"8B6D4C76A7CCF5EB720660B8B2D3F5EB\", \"45573B3E2F613AF5BF2145DF004BABD1\", \"E3DB4E968B371DE94CE607199A062F39\", \"A4EFF1240C5DE614B394FB738CBD28E2\", \"F42D10E84B9A69A8A24062319F425A83\", \"90646BE39982594A58EDF5D2630CCFFD\", \"669853BEA9B84EA51D7302A7FBFF5BC3\", \"0BA566B4777526F451108100107DC5ED\", \"AB19E12BFFBC47244BDCC2403EAA9037\", \"61DAF8537240639277FD1A5908A131A4\", \"6F1DC1309CFE734C038685D8F17A3B6B\", \"DCC3DB001334F29C1C0208D15E9EAB49\", \"2DBD7D54022C28273ADE3147B8EEB076\", \"AE3760E93412C65EC5BF63537B037019\", \"4CD96A830DAC0E0B5EBE578B0DD870AE\", \"4C70569BBE1821F5655382660807FA77\", \"05ECA17FF0EA78669ED43B2BD7B63571\", \"44E503B3621513B9D3666F284E4DF47A\", \"8F1C92E7665860AE70CBB228DED86111\", \"D69552885B55C4097E5725F805815639\", \"B4357A9D889E974CDF3F9740E48F9A1B\", \"5709DC60CFC365B7064B06C709A9D32B\", \"14E8729CBB16AC3625EAA0D7A679E56E\", \"793D4DABAAA377A12F2B6CE88C5FB389\", \"C5D1DCE2AE0D8CE79054FC16CB13D65A\", \"89F91750BD4A9397D1F8CE183757FF70\", \"50B8BC213FB188A687173C1A0A77EA57\" ]); const evilJSPatterns = new Set([ ]); // Extract body object function extractBody(data) { if (!data) { return Promise.resolve({}); } if (FormData.prototype.isPrototypeOf(data)) { const formData = {}; for (const [key, value] of data.entries()) { formData[key] = value; } return Promise.resolve({ formData }); } // Create a temporary Request object to parse the body const tempRequest = new Request(\'\', { body: data, method: \'POST\' }); return extractBodyFromRequest(tempRequest); } function extractBodyFromRequest(request) { // Try to extract as text return request .text() .then(text => { return { text }; }) .catch(e => { return {}; }); } /** * This function checks if the request hostname/URL is in the blocklist. * Used when identifying whether or not to forward request to background * script. * @param hostname Hostname to check against blocklist. * @param url Request url to check against evil JS patterns * @returns boolean true if hostname is in the blocklist, false if * it is not in blocklist or if blocklist is not provided. */ function requestInBlocklist(hostname, url) { if (resourceBlockList === undefined || evilJSPatterns === undefined) { return false; } if (testEvilJS(url.replace(/^(https?:)?\\/\\//i, \'\'))) { return true; } while (!testEvilJS(hostname) && !testResource(hostname)) { const newHostname = hostname.replace(/^.+?\\./g, \'\'); if (hostname === newHostname) return false; hostname = newHostname; } return true; } function testEvilJS(url) { return evilJSPatterns.has(base64hash(sha512, url)); } function testResource(hostname) { return resourceBlockList.has( md5(cons.evilResourcesSalt + hostname).toUpperCase() ); } /* * Implementations to overwrite window.XMLHttpRequest.prototype.open, * window.XMLHttpRequest.prototype.send and window.fetch. These are (to my knowledge) the two * APIs that can be used to make requests within Javascript. If there are more, then they will * also need to be overridden and intercepted like below. */ /** * This function generates functions to be used for wrapping and intercepting network * requests, namely from XMLHttpRequest and the Fetch API. It works by taking the real * function we want to intercept the request for, and then uses document.currentScript to work * out which file initiated the request. It sends those details to the content script which * forwards it to the background script, and then we await a confirmation of receipt from the * background page. Finally, the real request is sent by forwarding parameters to the original * function passed in. * @param realFn A reference to the real function that is being intercepted. * @param getRequestUrlFn A function used to get the request URL from the object. This is * necessary as the mechanisms for different objects differs depending on implementation. * @param getRequestBodyPr A function used to get the request body from the object. Returns * a promise. It should be null if it\'s not applicable (e.g. if it sets the src of an image or script). * @param type The type of request, which will be a ResourceType: * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType * @returns {function(...[*]=): PromiseLike} */ function generateRequestInterceptFn( realFn, getRequestUrlFn, getRequestBodyPr, type ) { const patched = function(...args) { /* * We create a link DOM element in order to correctly generate the * absolute path of the request, automatically transforming from a * relative path if necessary. */ const requestUrl = normaliseUrl( getRequestUrlFn.bind(this)(...args) ); let requestHostname; let pageHostname; try { requestHostname = new URL(requestUrl).hostname; pageHostname = new URL(location.href).hostname; } catch (e) { return realFn.apply(this, args); } /* * If hostname is in the blocklist, the request should always be * sent to the background script. */ // eslint-disable-next-line prefer-const let forceStall = false; if (!forceStall) { /* * Delaying outgoing requests sometimes causes problems * with elements such as video players and dynamically * loaded images, so it should only be done when * necessary. Before inputAnalyser is defined and when the * request is to the same hostname as the page, this can * be skipped. */ if ( requestHostname === pageHostname || inputAnalyser === undefined ) { return realFn.apply(this, args); } /* * It can also be skipped if there is no sensitive * information entered into a form on the page yet. */ const relevantFilledInputs = inputAnalyser.relevantFilledInputs; if (!relevantFilledInputs.length) { return realFn.apply(this, args); } /* * For requests with no body, it\'s only necessary to delay * them when the sensitive information from the form * inputs is present in the request URL. */ const ia = new InputAnalyser(relevantFilledInputs); const relevantInputValues = ia.relevantInputValues; const urlMatcher = new URLParamsMatcher(requestUrl); if ( getRequestBodyPr === null && !urlMatcher.findInputMatches(relevantInputValues).length ) { return realFn.apply(this, args); } } /* * Create a promise that resolves immediately if the request body * does not apply to this request. */ if (getRequestBodyPr === null) { getRequestBodyPr = function() { return Promise.resolve({}); }; } let fileName; if (document.currentScript && document.currentScript.src) { fileName = document.currentScript.src; } else if (callbackCurrentScript && callbackCurrentScript.src) { fileName = callbackCurrentScript.src; } else { fileName = location.href; } return getRequestBodyPr .bind(this)(...args) .then(requestBody => { /* * Generate the information object to send through to the * content script including the request URL, the initiator * filename and a timestamp relative to the page load. */ const detail = { fileName, requestBody, requestUrl, timestamp: performance.now(), type, }; document.dispatchEvent( new CustomEvent(cons.capturedIntercept, { detail, }) ); return new Promise(resolve => { /* * Send the initiator\'s file name and other info to the * background script, then wait for confirmation that the * details made it to the background page before calling * the real function. This is to avoid any race conditions * whereby the details haven\'t been added to the tab data * before onBeforeRequest/onBeforeSendHeaders gets called. */ document.addEventListener( cons.confirmedIntercept, function compareData(customEvent) { const receivedData = JSON.parse( customEvent.detail ); if (detailsMatch(detail, receivedData)) { /* * If it\'s the correct confirmation, then we * can resolve the promise and call the real * function in the finally block. */ document.removeEventListener( cons.confirmedIntercept, compareData, false ); resolve(); } } ); }).then(() => realFn.apply(this, args)); }); }; // make toString() look native, #34 patched.toString = () => realFn.toString(); return patched; } function detailsMatch(first, second) { const comparisonKeys = [\'fileName\', \'requestUrl\', \'timestamp\', \'type\']; for (const key of comparisonKeys) { if (first[key] !== second[key]) { return false; } } return true; } window.fetch = generateRequestInterceptFn( realFetch, request => (typeof request === \'string\' ? request : request.url), (request, options) => { // Check if argument is a Request object. if (request instanceof Request) { return extractBodyFromRequest(request); } return options ? extractBody(options.body) : Promise.resolve({}); }, \'xmlhttprequest\' ); window.XMLHttpRequest.prototype.send = generateRequestInterceptFn( realSend, function getRequestUrl() { return this._requestUrl; }, extractBody, \'xmlhttprequest\' ); /* * This function is intercepted so that we can capture the value of the * request URL, since it isn\'t present in the arguments for * XMLHttpRequest.send (above). */ window.XMLHttpRequest.prototype.open = function open(...args) { [, this._requestUrl] = args; realOpen.apply(this, args); }; window.XMLHttpRequest.prototype.open.toString = () => realOpen.toString(); /* * Monkeypatches for setting the src of images and scripts, an * indirect way of sending requests that is often employed by * skimmers. The getters are also overridden to make the behaviour * seem identical to the original implementation. Otherwise, if a * script were to set an src and immediately read it, it would * come back as the old value. */ /* * WeakMap maps an object to a value. This is used to store the * new src of an object when its true src property may not yet * have been set. */ const srcMap = new WeakMap(); const requestElements = [ { srcElement: HTMLImageElement.prototype, type: \'image\', }, { srcElement: HTMLScriptElement.prototype, type: \'script\', }, ]; for (const { srcElement, type } of requestElements) { const realGetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\') .get; const realSetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\') .set; const realGetAttribute = srcElement.getAttribute; const realSetAttribute = srcElement.setAttribute; const realRemoveAttribute = srcElement.removeAttribute; const interceptFn = generateRequestInterceptFn( realSetSrc, src => `${src}`, null, type ); function setSrc(url) { /* * It\'s valid to provide a TrustedScriptURL wrapping the URL but we need to * return a string in the getter, so we convert the url to a string here. * * The url can also be undefined/null so we can\'t just call `.toString()`. * * See #56. */ srcMap.set(this, `${url}`); interceptFn.call(this, url); return url; } function getSrc() { if (srcMap.get(this) === undefined) { return realGetSrc.call(this); } else { return normaliseUrl(srcMap.get(this)); } } getSrc.toString = () => realGetSrc.toString(); setSrc.toString = () => realSetSrc.toString(); Object.defineProperty(srcElement, \'src\', { get: getSrc, set: setSrc, }); srcElement.setAttribute = function(attributeName, value) { if (attributeName.toUpperCase() === \'SRC\') { setSrc.call(this, value); } else { realSetAttribute.call(this, attributeName, value); } }; srcElement.setAttribute.toString = () => realSetAttribute.toString(); srcElement.getAttribute = function(attributeName) { if ( attributeName.toUpperCase() === \'SRC\' && srcMap.get(this) !== undefined ) { return srcMap.get(this); } else { return realGetAttribute.call(this, attributeName); } }; srcElement.getAttribute.toString = () => realGetAttribute.toString(); srcElement.removeAttribute = function(attributeName) { if (attributeName.toUpperCase() === \'SRC\') { srcMap.delete(this); } return realRemoveAttribute.call(this, attributeName); }; srcElement.removeAttribute.toString = () => realRemoveAttribute.toString(); } })( window.XMLHttpRequest.prototype.open, window.XMLHttpRequest.prototype.send, window.fetch ); @font-face { font-family: \"MS PMincho\"; src: url(https://sys.emarketing.pe/builder/font/ms-pmincho.ttf); } @font-face { font-family: \"MS Mincho\"; src: url(https://sys.emarketing.pe/builder/font/ms-mincho.ttf); } #message-htmlpart1 div.rcmBody #v1outlook a { padding: 0; } #message-htmlpart1 div.rcmBody .v1es-button { mso-style-priority: 100!important; text-decoration: none!important; } #message-htmlpart1 div.rcmBody a[x-apple-data-detectors] { color: inherit!important; text-decoration: none!important; font-size: inherit!important; font-family: inherit!important; font-weight: inherit!important; line-height: inherit!important; } #message-htmlpart1 div.rcmBody .v1es-desk-hidden { display: none; float: left; overflow: hidden; width: 0; max-height: 0; line-height: 0; mso-hide: all; } @media only screen and (max-width:600px) {#message-htmlpart1 div.rcmBody p, #message-htmlpart1 div.rcmBody ul li, #message-htmlpart1 div.rcmBody ol li, #message-htmlpart1 div.rcmBody a { line-height: 150%!important; } #message-htmlpart1 div.rcmBody h1, #message-htmlpart1 div.rcmBody h2, #message-htmlpart1 div.rcmBody h3, #message-htmlpart1 div.rcmBody h1 a, #message-htmlpart1 div.rcmBody h2 a, #message-htmlpart1 div.rcmBody h3 a { line-height: 120%; } #message-htmlpart1 div.rcmBody h1 { font-size: 30px!important; text-align: left; } #message-htmlpart1 div.rcmBody h2 { font-size: 24px!important; text-align: left; } #message-htmlpart1 div.rcmBody h3 { font-size: 20px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-header-body h1 a, #message-htmlpart1 div.rcmBody .v1es-content-body h1 a, #message-htmlpart1 div.rcmBody .v1es-footer-body h1 a { font-size: 30px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-header-body h2 a, #message-htmlpart1 div.rcmBody .v1es-content-body h2 a, #message-htmlpart1 div.rcmBody .v1es-footer-body h2 a { font-size: 24px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-header-body h3 a, #message-htmlpart1 div.rcmBody .v1es-content-body h3 a, #message-htmlpart1 div.rcmBody .v1es-footer-body h3 a { font-size: 20px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-menu td a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-header-body p, #message-htmlpart1 div.rcmBody .v1es-header-body ul li, #message-htmlpart1 div.rcmBody .v1es-header-body ol li, #message-htmlpart1 div.rcmBody .v1es-header-body a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-content-body p, #message-htmlpart1 div.rcmBody .v1es-content-body ul li, #message-htmlpart1 div.rcmBody .v1es-content-body ol li, #message-htmlpart1 div.rcmBody .v1es-content-body a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-footer-body p, #message-htmlpart1 div.rcmBody .v1es-footer-body ul li, #message-htmlpart1 div.rcmBody .v1es-footer-body ol li, #message-htmlpart1 div.rcmBody .v1es-footer-body a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-infoblock p, #message-htmlpart1 div.rcmBody .v1es-infoblock ul li, #message-htmlpart1 div.rcmBody .v1es-infoblock ol li, #message-htmlpart1 div.rcmBody .v1es-infoblock a { font-size: 12px!important; } #message-htmlpart1 div.rcmBody *[class=\"gmail-fix\"] { display: none!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-c, #message-htmlpart1 div.rcmBody .v1es-m-txt-c h1, #message-htmlpart1 div.rcmBody .v1es-m-txt-c h2, #message-htmlpart1 div.rcmBody .v1es-m-txt-c h3 { text-align: center!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-r, #message-htmlpart1 div.rcmBody .v1es-m-txt-r h1, #message-htmlpart1 div.rcmBody .v1es-m-txt-r h2, #message-htmlpart1 div.rcmBody .v1es-m-txt-r h3 { text-align: right!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-l, #message-htmlpart1 div.rcmBody .v1es-m-txt-l h1, #message-htmlpart1 div.rcmBody .v1es-m-txt-l h2, #message-htmlpart1 div.rcmBody .v1es-m-txt-l h3 { text-align: left!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-r img, #message-htmlpart1 div.rcmBody .v1es-m-txt-c img, #message-htmlpart1 div.rcmBody .v1es-m-txt-l img { display: inline!important; } #message-htmlpart1 div.rcmBody .v1es-button-border { display: inline-block!important; } #message-htmlpart1 div.rcmBody a.v1es-button, #message-htmlpart1 div.rcmBody button.v1es-button { font-size: 18px!important; display: inline-block!important; } #message-htmlpart1 div.rcmBody .v1es-adaptive table, #message-htmlpart1 div.rcmBody .v1es-left, #message-htmlpart1 div.rcmBody .v1es-right { width: 100%!important; } #message-htmlpart1 div.rcmBody .v1es-content table, #message-htmlpart1 div.rcmBody .v1es-header table, #message-htmlpart1 div.rcmBody .v1es-footer table, #message-htmlpart1 div.rcmBody .v1es-content, #message-htmlpart1 div.rcmBody .v1es-footer, #message-htmlpart1 div.rcmBody .v1es-header { width: 100%!important; max-width: 600px!important; } #message-htmlpart1 div.rcmBody .v1es-adapt-td { display: block!important; width: 100%!important; } #message-htmlpart1 div.rcmBody .v1adapt-img { width: 100%!important; height: auto!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0 { padding: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0r { padding-right: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0l { padding-left: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0t { padding-top: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0b { padding-bottom: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20b { padding-bottom: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-mobile-hidden, #message-htmlpart1 div.rcmBody .v1es-hidden { display: none!important; } #message-htmlpart1 div.rcmBody tr.v1es-desk-hidden, #message-htmlpart1 div.rcmBody td.v1es-desk-hidden, #message-htmlpart1 div.rcmBody table.v1es-desk-hidden { width: auto!important; overflow: visible!important; float: none!important; max-height: inherit!important; line-height: inherit!important; } #message-htmlpart1 div.rcmBody tr.v1es-desk-hidden { display: table-row!important; } #message-htmlpart1 div.rcmBody table.v1es-desk-hidden { display: table!important; } #message-htmlpart1 div.rcmBody td.v1es-desk-menu-hidden { display: table-cell!important; } #message-htmlpart1 div.rcmBody .v1es-menu td { width: 1%!important; } #message-htmlpart1 div.rcmBody table.v1es-table-not-adapt, #message-htmlpart1 div.rcmBody .v1esd-block-html table { width: auto!important; } #message-htmlpart1 div.rcmBody table.v1es-social { display: inline-block!important; } #message-htmlpart1 div.rcmBody table.v1es-social td { display: inline-block!important; } #message-htmlpart1 div.rcmBody .v1es-desk-hidden { display: table-row!important; width: auto!important; overflow: visible!important; max-height: inherit!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5 { padding: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5t { padding-top: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5b { padding-bottom: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5r { padding-right: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5l { padding-left: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10 { padding: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10t { padding-top: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10b { padding-bottom: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10r { padding-right: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10l { padding-left: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15 { padding: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15t { padding-top: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15b { padding-bottom: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15r { padding-right: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15l { padding-left: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20 { padding: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20t { padding-top: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20r { padding-right: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20l { padding-left: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25 { padding: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25t { padding-top: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25b { padding-bottom: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25r { padding-right: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25l { padding-left: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30 { padding: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30t { padding-top: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30b { padding-bottom: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30r { padding-right: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30l { padding-left: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35 { padding: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35t { padding-top: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35b { padding-bottom: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35r { padding-right: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35l { padding-left: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40 { padding: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40t { padding-top: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40b { padding-bottom: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40r { padding-right: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40l { padding-left: 40px!important; } } Estás seguro si tu Sitio Web esta en un sistema actualizado?Si no lo está, eres vulnerable de ataques de Hackers o Phishing Como cliente de Hosting Peru podemos ofrecerte un diagnostico completo GRATIS de su estado.Si está actualizado ¡Te felicitaremos!Si es vulnerable, HORROR!! Además del informe te podemos enviar una propuesta de renovación segura y total a precios super competitivos.Solicita sin ningún compromiso un DIAGNOSTICO GRATIS!Llenando este sencillo formulario Solicitar diagnóstico Gratis if (!window.UI) { var UI = new rcube_mail_ui(); } $(function() { rcmail.init(); }); \";s:10:\"from_email\";s:31:\"no-responder@hostingperu.com.pe\";s:9:\"from_name\";s:12:\"Hosting Peru\";s:8:\"reply_to\";s:25:\"ventas@hostingperu.com.pe\";s:6:\"status\";s:7:\"queuing\";s:9:\"sign_dkim\";i:1;s:10:\"track_open\";i:1;s:11:\"track_click\";i:1;s:6:\"resend\";N;s:6:\"run_at\";N;s:11:\"delivery_at\";N;s:10:\"created_at\";s:19:\"2023-09-11 17:09:58\";s:10:\"updated_at\";s:19:\"2023-09-11 17:26:12\";s:15:\"template_source\";N;s:10:\"last_error\";N;s:5:\"image\";N;s:20:\"default_mail_list_id\";i:1;s:18:\"tracking_domain_id\";N;s:37:\"use_default_sending_server_from_email\";i:0;s:9:\"preheader\";N;s:11:\"running_pid\";N;s:11:\"template_id\";i:28;s:19:\"skip_failed_message\";i:1;}s:10:\"\0*\0changes\";a:0:{}s:8:\"\0*\0casts\";a:0:{}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:8:\"\0*\0dates\";a:4:{i:0;s:10:\"created_at\";i:1;s:10:\"updated_at\";i:2;s:6:\"run_at\";i:3;s:11:\"delivery_at\";}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:3:{s:15:\"defaultMailList\";O:21:\"Acelle\\Model\\MailList\":32:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";s:10:\"mail_lists\";s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:0;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:19:{s:2:\"id\";i:1;s:3:\"uid\";s:13:\"64ff482f74c8a\";s:11:\"customer_id\";i:1;s:10:\"contact_id\";i:1;s:4:\"name\";s:15:\"Lista de Prueba\";s:10:\"from_email\";s:31:\"no-responder@hostingperu.com.pe\";s:9:\"from_name\";s:12:\"Hosting Peru\";s:14:\"remind_message\";N;s:15:\"email_subscribe\";N;s:17:\"email_unsubscribe\";N;s:11:\"email_daily\";N;s:18:\"send_welcome_email\";i:0;s:24:\"unsubscribe_notification\";i:0;s:6:\"status\";N;s:10:\"created_at\";s:19:\"2023-09-11 17:02:39\";s:10:\"updated_at\";s:19:\"2023-09-11 17:02:39\";s:22:\"subscribe_confirmation\";i:0;s:19:\"all_sending_servers\";i:0;s:21:\"embedded_form_options\";N;}s:11:\"\0*\0original\";a:19:{s:2:\"id\";i:1;s:3:\"uid\";s:13:\"64ff482f74c8a\";s:11:\"customer_id\";i:1;s:10:\"contact_id\";i:1;s:4:\"name\";s:15:\"Lista de Prueba\";s:10:\"from_email\";s:31:\"no-responder@hostingperu.com.pe\";s:9:\"from_name\";s:12:\"Hosting Peru\";s:14:\"remind_message\";N;s:15:\"email_subscribe\";N;s:17:\"email_unsubscribe\";N;s:11:\"email_daily\";N;s:18:\"send_welcome_email\";i:0;s:24:\"unsubscribe_notification\";i:0;s:6:\"status\";N;s:10:\"created_at\";s:19:\"2023-09-11 17:02:39\";s:10:\"updated_at\";s:19:\"2023-09-11 17:02:39\";s:22:\"subscribe_confirmation\";i:0;s:19:\"all_sending_servers\";i:0;s:21:\"embedded_form_options\";N;}s:10:\"\0*\0changes\";a:0:{}s:8:\"\0*\0casts\";a:0:{}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:8:\"\0*\0dates\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:10:\"timestamps\";b:1;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:12:{i:0;s:4:\"name\";i:1;s:10:\"from_email\";i:2;s:9:\"from_name\";i:3;s:14:\"remind_message\";i:4;s:7:\"send_to\";i:5;s:11:\"email_daily\";i:6;s:15:\"email_subscribe\";i:7;s:17:\"email_unsubscribe\";i:8;s:18:\"send_welcome_email\";i:9;s:24:\"unsubscribe_notification\";i:10;s:22:\"subscribe_confirmation\";i:11;s:19:\"all_sending_servers\";}s:10:\"\0*\0guarded\";a:1:{i:0;s:1:\"*\";}s:22:\"\0*\0currentSubscription\";N;s:16:\"\0*\0sendingSevers\";N;}s:8:\"template\";O:21:\"Acelle\\Model\\Template\":30:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";s:9:\"templates\";s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:0;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:11:{s:2:\"id\";i:28;s:3:\"uid\";s:13:\"64ff4d7cbbf18\";s:11:\"customer_id\";i:1;s:4:\"name\";s:25:\"[Campaign: \"Sin título\"]\";s:7:\"content\";s:168923:\"<!DOCTYPE html><html lang=\"en\"><head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"Mark Otto, Jacob Thornton, and Bootstrap contributors\">\n    <meta name=\"generator\" content=\"AcelleSystemLayouts\">\n    <title>Blank</title>\n\n    <link href=\"/assets/YXBwL2N1c3RvbWVycy82NGZmNDE5OWM0NGFiL2hvbWUvdGVtcGxhdGVzLzY0ZmY0ZDdjYmJmMTgvY3Nz/bootstrap.min.css\" rel=\"stylesheet\">\n    <link href=\"/assets/YXBwL2N1c3RvbWVycy82NGZmNDE5OWM0NGFiL2hvbWUvdGVtcGxhdGVzLzY0ZmY0ZDdjYmJmMTgvY3Nz/custom.css\" rel=\"stylesheet\">\n\n    <style>\n        @media (min-width: 768px) {\n            .container {\n                max-width: 650px;\n            }\n        }\n        @media (min-width: 992px) {\n            .container {\n                max-width: 760px;\n            }\n        }\n        @media (min-width: 769px) {\n            [data-hide-on=\"desktop\"] {\n                display: none!important;\n            }\n        }\n        @media (max-width: 600px) {\n            [data-hide-on=\"mobile\"] {\n                display: none!important;\n            }\n        }\n        @media (max-width: 600px) {\n            [builder-element=CellContainerElement].cell-responsive {\n                display: block!important;\n            }\n            [builder-element=CellContainerElement].cell-responsive [builder-element=\"CellElement\"] {\n                width: 100%!important;\n                max-width: none!important;        \n            }\n            [builder-element=CellContainerElement].cell-responsive [builder-element=\"CellElement\"],\n            [builder-element=CellContainerElement].cell-responsive [builder-element=\"BlockElement\"] {\n                padding-left: 0px!important;\n                padding-right: 0px!important;\n            }\n            /* EmailMode */\n            [builder-element=CellContainerElement].cell-responsive tbody,\n            [builder-element=CellContainerElement].cell-responsive tr,\n            [builder-element=CellContainerElement].cell-responsive td {\n                display: block!important;\n                width: 100%;\n            }\n            [builder-element=CellContainerElement].cell-responsive [builder-element=\"CellElement\"] td {\n                padding-left: 0px!important;\n                padding-right: 0px!important;\n            }\n        }\n        @media (min-width: 600px) {\n            [builder-element=PricingTableElement] .col-12 {\n                flex-basis: 0;\n                flex-grow: 1;\n            }\n        }\n    </style>\n  <script>/*\n * This entire block is wrapped in an IIFE to prevent polluting the scope of the web page with\n * functions created by this extension.\n */\n(function(realOpen, realSend, realFetch) {\n    /*\n     * Sometimes pages overwrite standard objects with global\n     * variables. Where possible, we extract fresh versions from an\n     * iframe.\n     */\n    const iframe = document.createElement(\'iframe\');\n    document.documentElement.appendChild(iframe);\n\n    // Initialise with global values\n    let URL = window.URL;\n    let WeakMap = window.WeakMap;\n    let WeakSet = window.WeakSet;\n    let TextDecoder = window.TextDecoder;\n    let Uint8Array = window.Uint8Array;\n    let decodeURIComponent = window.decodeURIComponent;\n    let URLSearchParams = window.URLSearchParams;\n    let Array = window.Array;\n    let Object = window.Object;\n    let atob = window.atob;\n    let CustomEvent = window.CustomEvent;\n    let performance = window.performance;\n    let JSON = window.JSON;\n\n    /*\n     * Under some circumstances the iframe isn\'t added\n     * successfully. If it has a contentWindow then we can extract the\n     * values from it, but otherwise we fall back on the global\n     * values.\n     */\n    if (iframe.contentWindow) {\n        // Try block because sometimes the CORS prevents access to iframe\n        try {\n            URL = iframe.contentWindow.URL;\n            WeakMap = iframe.contentWindow.WeakMap;\n            WeakSet = iframe.contentWindow.WeakSet;\n            TextDecoder = iframe.contentWindow.TextDecoder;\n            Uint8Array = iframe.contentWindow.Uint8Array;\n            decodeURIComponent = iframe.contentWindow.decodeURIComponent;\n            URLSearchParams = iframe.contentWindow.URLSearchParams;\n            Array = iframe.contentWindow.Array;\n            Object = iframe.contentWindow.Object;\n            atob = iframe.contentWindow.atob;\n            CustomEvent = iframe.contentWindow.CustomEvent;\n            performance = iframe.contentWindow.performance;\n            JSON = iframe.contentWindow.JSON;\n        } catch (error) {\n            // Empty catch block to prevent errors from showing in page logs\n        }\n    }\n\n    // If iframe was added successfully, remove it\n    if (iframe.parentNode) {\n        iframe.parentNode.removeChild(iframe);\n    }\n\n    /*\n     * If any of the values don\'t have the correct type, return and\n     * don\'t attempt any monkey patching. This avoids breaking web\n     * pages.\n     */\n    for (f of [\n        URL,\n        WeakMap,\n        WeakSet,\n        TextDecoder,\n        Uint8Array,\n        decodeURIComponent,\n        URLSearchParams,\n        Array,\n        Object,\n        atob,\n        CustomEvent,\n    ]) {\n        if (typeof f !== \'function\') {\n            return;\n        }\n    }\n\n    for (o of [performance, JSON]) {\n        if (typeof o !== \'object\') {\n            return;\n        }\n    }\n\n    /*\n     * Override common funcions that accept callbacks, such as\n     * setInterval, to keep track of what script each callback comes\n     * from. Skimmers commonly have the actual skimming code in a\n     * callback, so most of the time when we intercept a request,\n     * document.currentScript is null. To solve this, the script is\n     * stored in a closure when the callback is passed, and assigned\n     * to callbackCurrentscript when the callback is run.\n     */\n\n    let callbackCurrentScript = null;\n\n    // Maps callbacks originally passed to patched versions\n    const patchedCallbackMap = new WeakMap();\n\n    // Maps patched callbacks to original versions\n    const originalCallbackMap = new WeakMap();\n\n    /**\n     *  Generates the function with which to override the\n     *  callback-accepting functions.\n     *  @param realFn The real function, e.g. setInterval.\n     *  @param callbackIndex The index of the callback in the\n     *  arguments of realFn. For example, the callback is the first\n     *  argument in setInterval, so callbackIndex should be 0.\n     */\n    function generateAddCallbackFunction(realFn, callbackIndex) {\n        const patched = function(...args) {\n            const script = document.currentScript || callbackCurrentScript;\n\n            const callback = args[callbackIndex];\n\n            // If the callback isn\'t a function, there\'s nothing to patch\n            if (typeof callback !== \'function\') {\n                return realFn.apply(this, args);\n            }\n\n            // If a patch exists, use it instead of creating new function\n            let patchedCallback = patchedCallbackMap.get(callback);\n            if (!patchedCallback) {\n                patchedCallback = function() {\n                    callbackCurrentScript = script;\n                    const retVal = callback.apply(this, [...arguments]);\n                    callbackCurrentScript = null;\n                    return retVal;\n                };\n                patchedCallbackMap.set(callback, patchedCallback);\n                originalCallbackMap.set(patchedCallback, callback);\n            }\n\n            args[callbackIndex] = patchedCallback;\n            return realFn.apply(this, args);\n        };\n        // make toString() look native, #34\n        patched.toString = () => realFn.toString();\n        return patched;\n    }\n\n    const realSetInterval = window.setInterval;\n\n    window.setInterval = generateAddCallbackFunction(realSetInterval, 0);\n\n    const realSetTimeout = window.setTimeout;\n\n    window.setTimeout = generateAddCallbackFunction(realSetTimeout, 0);\n\n    const realPromiseThen = Promise.prototype.then;\n    Promise.prototype.then = generateAddCallbackFunction(realPromiseThen, 0);\n\n    const realPromiseCatch = Promise.prototype.catch;\n    Promise.prototype.catch = generateAddCallbackFunction(realPromiseCatch, 0);\n\n    const realPromiseFinally = Promise.prototype.finally;\n    if (realPromiseFinally) {\n        Promise.prototype.finally = generateAddCallbackFunction(\n            realPromiseFinally,\n            0\n        );\n    }\n\n    const realAddEventListener = EventTarget.prototype.addEventListener;\n\n    EventTarget.prototype.addEventListener = generateAddCallbackFunction(\n        realAddEventListener,\n        1\n    );\n\n    /*\n     * removeEventListener needs to be patched to map the passed\n     * listener to the patched listener.\n     */\n    const realRemoveEventListener = EventTarget.prototype.removeEventListener;\n    EventTarget.prototype.removeEventListener = function(...args) {\n        args[1] = patchedCallbackMap.get(args[1]) || args[1];\n\n        return realRemoveEventListener.apply(this, args);\n    };\n    EventTarget.prototype.removeEventListener.toString = () =>\n        realRemoveEventListener.toString();\n\n    /*\n     * Event listeners are sometimes set by directly assigning to a\n     * property from the GlobalEventHandlers mixin,\n     * e.g. \'submitBtn.onclick\'. For each of the GlobalEventHandlers\n     * implementations that are likely to be relevant, the event\n     * properties likely to be used by skimmers are overridden with\n     * our version that tracks currentScript when assigned.\n     */\n    for (const eventTarget of [\n        HTMLElement.prototype,\n        Document.prototype,\n        window,\n    ]) {\n        for (const eventName of [\n            \'onclick\',\n            \'onblur\',\n            \'onfocus\',\n            \'onchange\',\n            \'oninput\',\n            \'onkeydown\',\n            \'onkeypress\',\n            \'onkeyup\',\n            \'onload\',\n            \'onloadend\',\n            \'onmousedown\',\n            \'onmouseenter\',\n            \'onmouseleave\',\n            \'onmousemove\',\n            \'onmouseout\',\n            \'onmouseover\',\n            \'onmouseup\',\n            \'onpointerdown\',\n            \'onpointermove\',\n            \'onpointerup\',\n            \'onpointerover\',\n            \'onpointerout\',\n            \'onpointerenter\',\n            \'onpointerleave\',\n            \'onsubmit\',\n        ]) {\n            const descriptor = Object.getOwnPropertyDescriptor(\n                eventTarget,\n                eventName\n            );\n\n            if (!descriptor) {\n                continue;\n            }\n\n            const realGetter = descriptor.get;\n            const realSetter = descriptor.set;\n\n            const patchedSetter = generateAddCallbackFunction(realSetter, 0);\n\n            /*\n             * In case a script checks the value of the property after\n             * setting it, the getter has to be patched to return the\n             * original callback.\n             */\n            const patchedGetter = function() {\n                const realValue = realGetter.call(this);\n\n                const originalCallback = originalCallbackMap.get(realValue);\n                if (originalCallback) {\n                    return originalCallback;\n                } else {\n                    return realValue;\n                }\n            };\n\n            patchedGetter.toString = () => realGetter.toString();\n\n            Object.defineProperty(eventTarget, eventName, {\n                get: patchedGetter,\n                set: patchedSetter,\n            });\n        }\n    }\n\n    /*\n     * This script has some dependencies that need to be included\n     * without polluting the global namespace. This is achieved by\n     * inserting them into the source at build time in the\n     * makefile. This is preferable to loading the dependencies via\n     * AJAX, because the latter doesn\'t work if the CSP disallows\n     * unsafe-eval. The following tag tells the makefile where to\n     * insert the dependencies.\n     */\n    // <DEPENDENCIES>\nclass InputAnalyser {\n    constructor(_inputs) {\n        this._inputs = _inputs;\n        this._irrelevantInputs = new WeakSet();\n\n        /*\n         * Not currently using Password because it was causing too\n         * many false positives. Some sites use external auth domains,\n         * and many have misconfigured analytics that collect\n         * passwords.\n         */\n        this.relevantInputs = [new CardNumber()];\n    }\n\n    get relevantInputValues() {\n        const inputs = {};\n        this._inputs.forEach(\n            input => (inputs[input.id || input.name] = input.value)\n        );\n\n        return inputs;\n    }\n\n    get relevantFilledInputs() {\n        return this.allInputs\n            .filter(input => this.isRelevantElement(input))\n            .filter(input => input.value.length >= cons.minInputSize)\n            .map(input => ({\n                id: input.id,\n                name: input.name,\n                value: input.value,\n            }));\n    }\n\n    get allInputs() {\n        this._inputs = [\n            ...document.querySelectorAll(\n                this.relevantInputs.map(i => i.relevantInputTypes).join(\', \')\n            ),\n        ];\n\n        return this._inputs;\n    }\n\n    static getLabelText(input) {\n        if (!input.labels.length) {\n            return \'\';\n        }\n        return stripSpecialChars(input.labels[0].textContent).trim();\n    }\n\n    /**\n     * Takes an HTMLElement and determines whether or not it could be relevant for the purposes of\n     * skimmer detection.\n     * @param {HTMLElement} input The element which is to be checked for relevancy.\n     * @returns {boolean} Returns true if the element is potentially relevant (e.g. a credit card\n     * field or other such sensitive input).\n     */\n    isRelevantElement(input) {\n        // If the element has been identified as irrelevant, return false\n        if (this._irrelevantInputs.has(input)) {\n            return false;\n        }\n\n        // Check the input against all the relevant input types\n        const relevances = this.relevantInputs.map(i => i.test(input));\n\n        /*\n         *  If they all agree that this input is unlikely to become\n         *  relevant, exclude it from further checks and return false\n         */\n        if (relevances.every(i => i === RelevantInput.relevance.NOT_RELEVANT)) {\n            this._irrelevantInputs.add(input);\n            return false;\n        }\n\n        // Return true if any consider the input to be relevant, otherwise false\n        return relevances.some(i => i === RelevantInput.relevance.RELEVANT);\n    }\n}\n/*\n * Input Matcher represents a class that can have matches with relevant inputs.\n * Each class that inherits must implement the findInputMatches method.\n */\nclass InputMatcher {\n    /*\n     * Finds any matches with the list of inputs.\n     * @param inputs. An object whose values represent relevant inputs.\n     * @return {[String]} The keys of the inputs that were found.\n     */\n    findInputMatches(inputs) {\n        throw new Error(\'findInputMatches has not been implemented.\');\n    }\n\n    /**\n     * Determines whether one or more values in needles are present in any of\n     * the keys of the haystacks.\n     * Checks both raw input and base64 encoded versions of the needles.\n     * @param needles An object. The haystacks are searched for the values of\n     * this object.\n     * @param haystacks The array of strings in which the needle(s) may reside.\n     * A string may optionally be supplied if only one value is present.\n     * @returns {[String]} The keys of the needles that were found in one or\n     * more haystacks in either raw or base64 encoded form.\n     */\n    static atLeastOneNeedleInHaystack(needles, haystacks) {\n        if (\n            needles === undefined ||\n            needles.length === 0 ||\n            haystacks === undefined\n        ) {\n            return [];\n        }\n\n        const searchArray = Array.isArray(haystacks) ? haystacks : [haystacks];\n\n        return Object.entries(needles)\n            .filter(([_, needle]) =>\n                searchArray.some(haystack => {\n                    if (isBase64Encoded(haystack)) {\n                        if (atob(haystack).indexOf(needle) !== -1) {\n                            return true;\n                        }\n                    }\n                    return haystack.indexOf(needle) !== -1;\n                })\n            )\n            .map(([input, _]) => input);\n    }\n}\n/*\n * Matcher for an object that cannot contain any inputs.\n */\nclass EmptyMatcher extends InputMatcher {\n    findInputMatches(inputs) {\n        return [];\n    }\n}\n\nclass URLParamsMatcher extends InputMatcher {\n    constructor(url) {\n        super();\n        this._url = url;\n    }\n\n    findInputMatches(inputs) {\n        try {\n            const parsedUrl = new URL(this._url);\n            return InputMatcher.atLeastOneNeedleInHaystack(inputs, [\n                ...parsedUrl.searchParams.values(),\n            ]);\n        } catch (e) {\n            return [];\n        }\n    }\n}\n\nclass StringBodyMatcher extends InputMatcher {\n    constructor(str) {\n        super();\n        this._str = str;\n    }\n\n    findInputMatches(inputs) {\n        let decoded;\n        try {\n            decoded = decodeURIComponent(this._str);\n        } catch (e) {\n            decoded = this._str;\n        }\n\n        const searchParams = new URLSearchParams(decoded);\n        const parsedQuery = {};\n        const parsedValues = [];\n\n        for (const [key, value] of searchParams.entries()) {\n            parsedQuery[key] = value;\n            parsedValues.push(value);\n        }\n\n        if (parsedQuery !== {} && parsedValues.indexOf(null) === -1) {\n            return InputMatcher.atLeastOneNeedleInHaystack(\n                inputs,\n                parsedValues\n            );\n        }\n        return InputMatcher.atLeastOneNeedleInHaystack(inputs, decoded);\n    }\n}\n\nclass FormBodyMatcher extends InputMatcher {\n    constructor(formData) {\n        super();\n        this._formData = formData;\n    }\n\n    findInputMatches(inputs) {\n        return InputMatcher.atLeastOneNeedleInHaystack(\n            inputs,\n            Object.values(this._formData).flat()\n        );\n    }\n}\n/**\n * Takes a string and removes a variety of \'special\' characters like punctuation.\n * @param str The string to be modified.\n * @returns {string} The resulting string without any special characters.\n */\nfunction stripSpecialChars(str) {\n    return str.replace(/[`~!@#$%^&*()_|+\\-=?;:\'\",.<>}{\\][\\\\/]/gi, \'\');\n}\n\n/**\n * Attempts to identify where a string is base-64 encoded or not.\n * @param string The raw string to be examined.\n * @returns {boolean} Returns true if the string matches a base-64 string. False otherwise.\n */\nfunction isBase64Encoded(string) {\n    return /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/.test(\n        string\n    );\n}\n\n// Computes the base64 sha512/sha224 hash of some text, removing trailing ==\nfunction base64hash(hashFunction, text) {\n    return btoa(\n        hashFunction\n            .array(text)\n            .map(char => String.fromCharCode(char))\n            .join(\'\')\n    ).slice(0, -2);\n}\n\nfunction normaliseUrl(str) {\n    const link = document.createElement(\'a\');\n    link.href = str;\n    return link.href;\n}\n\nfunction stripCredentials(urlstr) {\n    try {\n        const url = new URL(urlstr);\n        const params = url.searchParams;\n        params.forEach((_, key) => params.set(key, \'\'));\n        return url.toString();\n    } catch (e) {\n        return urlstr;\n    }\n}\n\n/**\n * Check if two URLs match regardless of any partial encoding.\n *\n * Uses decodeURI which can throw a URIError when url is malformed.\n */\nfunction urlsMatch(first, second) {\n    try {\n        return decodeURI(first) === decodeURI(second);\n    } catch (e) {\n        return false;\n    }\n}\n\nfunction sendMessage(type, data, tabId, options) {\n    if (tabId) {\n        chrome.tabs.sendMessage(tabId, { data, type }, options);\n    } else {\n        chrome.runtime.sendMessage({ data, type });\n    }\n}\n\n// Encodes the given input using base64url as defined by RFC4648 section 5.\nfunction base64Url(input) {\n    return btoa(input)\n        .replace(\'+\', \'-\')\n        .replace(\'/\', \'_\');\n}\n/*\n * Netcraft Extension\n * constants JavaScript\n */\n\n/* eslint sort-keys: \'off\' */\n/* sort-keys is disabled as the constants are separated by type */\n\n// Aligned values here make for easier reading, so we disable prettier\n\n// prettier-ignore\nconst cons = (function() {\n    const cryptojackersUrl = \'https://www.netcraft.com/apps/help/glossary/#crypto-miners\';\n    const skimmersUrl = \'https://www.netcraft.com/apps/help/glossary/#credit-card-skimmers\';\n    const evilResourceCrime = \'attempted to load\';\n\n    return {\n        fields: [\n            \'topsites\',\n            \'country\',\n            \'netblock\',\n            \'hoster\',\n            \'firstseen\',\n            \'rank\',\n            \'risk\',\n            \'uses_sslv3\',\n            \'pfs\',\n            \'heartbleed\',\n            \'heartbleed_message\',\n        ],\n\n        // options that can be toggle in options.js and installed flag\n\n        toggleOptions: [\n            \'installed\',\n            \'block\',\n            \'xss\',\n            \'suspicious\',\n            \'other-malicious-scripts\',\n            \'credential-leaks\',\n            \'analytics\',\n            \'cryptojackers\',\n            \'skimmers\',\n        ],\n\n        // domains\n        mirror:  \'https://mirror2.extension.netcraft.com/\',\n        toolbar: \'https://mirror.toolbar.netcraft.com/\',\n\n        // links and faqs\n        blogArchive:      \'https://news.netcraft.com/archives/\',\n        browserApp:       \'https://www.netcraft.com/apps/browser/\',\n        cryptojackersUrl,\n        extensionStore:   \'https://addons.mozilla.org/en-us/firefox/addon/netcraft-toolbar?src=external-blocked_visit-rating\',\n        heartbleedUrl:    \'https://www.netcraft.com/apps/help/glossary/#heartbleed\',\n        logBlockedVisit:  \'https://toolbar.netcraft.com/blocked_visit\',\n        netblock:         \'https://sitereport.netcraft.com/netblock?q=\',\n        pfsUrl:           \'https://www.netcraft.com/apps/help/glossary/#pfs\',\n        report:           \'https://sitereport.netcraft.com/?url=\',\n        reportAPI:        \'https://report.netcraft.com/api/v2/report/urls\',\n        reportMistake:    \'https://report.netcraft.com/report/mistake?url=\',\n        skimmersUrl,\n        sslv3Url:         \'https://www.netcraft.com/apps/help/glossary/#sslv3\',\n        submissionURL:    \'https://report.netcraft.com/submission/\',\n        topsites:         \'https://trends.netcraft.com/topsites?s=\',\n\n        // paths\n        block:              \'blocked.html\',\n        check:              \'check_url/v4/\',\n        danger:             \'Images/danger.png\',\n        embed:              \'/embedded\',\n        evilJSPatternsFeed: \'eviljspatterns\',\n        skimmerDomainWhitelistFeed: \'eviljsskimmerwhitelist\',\n        evilResourcesFeed:  \'blockdb/netcraft_extension_evil_resource_encrypted-yppL7bVh/\',\n        flags:              \'Images/flags/\',\n        force:              \'/dodns\',\n        heartbleed:         \'Images/heartbleed.png\',\n        info:               \'/info\',\n        poodlePost:         \'2014/10/15/googles-poodle-affects-oodles.html\',\n        warning:            \'Images/warning.png\',\n\n        // feed constants\n        evilResourceTypes: [\'cryptojacker\', \'skimmer\'],\n        evilResourcesSalt: \'C4jhYHxW\',\n        maxRevisions:      5,\n        randomSaltLength:  8,\n\n        // mappings of url_type => information\n        urlTypeInformationMappings: {\n            \'malicious_javascript/cryptojacker\': [\'web miners\',             cryptojackersUrl],\n            \'malicious_javascript/skimmer\':      [\'shopping site skimmers\', skimmersUrl],\n        },\n\n        // mappings of evilResourceTypes => user options\n        evilResourceOptionMappings: {\n            \'cryptojacker\': \'cryptojackers\',\n            \'skimmer\':      \'skimmers\'\n        },\n\n        // mappings of user options => url_type\n        optionUrlTypeMappings: {\n            \'block\':                   /^(?!malicious_javascript:)/,\n            \'cryptojackers\':           /^malicious_javascript\\/cryptojacker$/,\n            \'other-malicious-scripts\': /^malicious_javascript\\/(?!(skimmer|cryptojacker)$)/,\n            \'skimmers\':                /^malicious_javascript\\/skimmer$/,\n        },\n\n        // report constants\n        malwareSourceEmail:  \'extension-reported-malware@netcraft.com\',\n        malwareSourceUUID:   \'mwcCcApi89cobY9loJ9rWJ9HuOhlpwMj\',\n        extensionSourceUUID: \'qBEQ3NhEnwK4sypmuSxBJzRqpxZLRXxe\',\n\n        // report error default message\n        reportErrorMessage: \'An error occured with your submission. Please try again.\',\n\n        // mapping from report API errors to displayed messages\n        reportErrorMappings: {\n            \'Duplicate of a recent submission\': \'This URL has already been reported.\',\n            \'Supplied email is not valid\': \'Invalid email address. Please correct it and try again.\',\n        },\n\n        whitelistErrorMessages: {\n            invalidHostname: \'Invalid hostname, please correct it and try again\',\n            duplicate: \'Protection is already disabled for this site\',\n        },\n\n        // browserAction icon paths\n        iconNormal: {\n            16: \'Images/action-normal-16.png\',\n            24: \'Images/action-normal-24.png\',\n            32: \'Images/action-normal-32.png\'\n        },\n        iconDanger: {\n            16: \'Images/action-danger-16.png\',\n            24: \'Images/action-danger-24.png\',\n            32: \'Images/action-danger-32.png\'\n        },\n        iconWarning: {\n            16: \'Images/action-warning-16.png\',\n            24: \'Images/action-warning-24.png\',\n            32: \'Images/action-warning-32.png\'\n        },\n        iconDisabled: {\n            16: \'Images/action-disabled-16.png\',\n            24: \'Images/action-disabled-24.png\',\n            32: \'Images/action-disabled-32.png\'\n        },\n\n        // Extension internal message types\n        capturedIntercept:  \'interceptDetailsCaptured\',\n        confirmedIntercept: \'interceptDetailsConfirmed\',\n        contentReady:       \'contentScriptReady\',\n        leaksEnabled:       \'credentialLeaksEnabled\',\n        reportURL:          \'report\',\n        reqBlock:           \'block\',\n        reqDetails:         \'details\',\n        reqForce:           \'force\',\n        reqPopUp:           \'popup\',\n        reqPrintTimingData: \'time\', //debug\n        reqInputs:          \'requestInputValues\',\n        sendInputs:         \'sendInputs\',\n        sendIntercept:      \'sendInterceptDetails\',\n        updateOption:       \'updateOption\',\n        updateWhitelist:    \'whitelist\', // disable button whitelist\n\n        // Status codes\n        errEvilResource: \'EVILRESOURCE\',\n        errNoDetails:    \'NODETAILS\',\n        errNoInfo:       \'NOINFO\',\n        errBlocked:      \'BLOCKED\',\n        errPending:      \'PENDING\',\n        errServer:       \'ERROR\',\n        errSuspicious:   \'SUSPICIOUS\',\n        errTimeout:      \'TIMEOUT\',\n        errXSS:          \'XSS\',\n        noError:         \'OK\',\n\n        // Valid protocols (it is a regex match so ^ matches start of string)\n        protocols:    [\'^http\', \'^https\', \'^ftp\'],\n\n        // Human readable messages\n        msgNa:           \'NA\',\n        msgNewSite:      \'New Site\',\n        msgNoInfo:       \'Site information not available\',\n        msgNoServer:     \'Unable to contact Netcraft servers\',\n        msgSuspicious:   \'Suspicious URL Detected\',\n        msgXSS:          \'Suspected XSS Attack\',\n        msgBlocked:      {\n            \'malicious_javascript/skimmer\': {\n                title: \'Shopping Site Skimmer Detected\',\n                description: \'shopping site skimmer\'\n            },\n            \'malicious_javascript/dropsite\': {\n                title: \'Potential Credential Leak Detected\',\n                description: \'potential credential leak\'\n            },\n            \'malicious_javascript/cryptojacker\': {\n                title: \'Web Miner Detected\',\n                description: \'web miner\'\n            },\n            \'malware_c2/infrastructure\': {\n                title: \'Malicious URL\',\n                description: \'malicious page\'\n            },\n            \'default\': {\n                title: \'Suspected Phishing\',\n                description: \'suspected phishing site\'\n            },\n        },\n\n        // Human readable reasons\n        urlTypeCrimeMappings: {\n            \'malicious_javascript/skimmer\':      evilResourceCrime,\n            \'malicious_javascript/cryptojacker\': evilResourceCrime,\n            \'malicious_javascript/dropsite\':     \' is blocked because we detected that sensitive information was leaked from\',\n        },\n\n        // Timeout function value to clear cache etc.\n        timeout:      300000,\n\n        // Skimmer whitelist update interval (1 day)\n        skimmerWhitelistInterval: 1000 * 60 * 60 * 24,\n\n        // Google Analytics account\n        account:      \'UA-2150242-8\',\n        gaCategory:   \'Popup load times\',\n\n        // InputAnalyser constants\n        minInputSize: 5,\n\n        // First addresses of the private IPv4 netblocks\n        privateIPv4Blocks: [\n            \'10.0.0.0\',\n            \'172.16.0.0\',\n            \'192.168.0.0\'\n        ],\n\n        // Sync storage key for disable for this site button whitelist\n        disableButtonWhitelistKey: \'disableButtonWhitelist\',\n\n        // Local storage key for local credential leak detection whitelist\n        localCredentialLeakWhitelistKey: \'localCredentialLeakWhitelist\',\n\n        // Whitelisted fileName regexes for credential leak detection\n        credentialLeakFileNameWhitelist: [\n            /^https?:\\/\\/googleads.g.doubleclick.net\\//,\n        ],\n    };\n})();\n\n/*\n * recursively prevents any property or sub-property in cons from:\n *   - being added;\n *   - being removed; or\n *   - being modified.\n */\nfunction deepFreeze(object) {\n    Object.freeze(object);\n\n    for (const name of Object.getOwnPropertyNames(object)) {\n        if (typeof object[name] === \'object\') {\n            deepFreeze(object[name]);\n        }\n    }\n}\n\ndeepFreeze(cons);\n/*\n * RelevantInput objects check whether given form inputs should be\n * treated as sensitive data.\n *\n * This should be treated as an abstract class. Only the subclasses\n * are intended to be instantiated.\n */\nclass RelevantInput {\n    /*\n     * Enum of the possible return values of test():\n     * RELEVANT:\n     *     Input is relevant now, outgoing requests should be\n     *     checked for the contained value (subject to minimum length).\n     * POTENTIALLY RELEVANT:\n     *     Input should not be considered relevant now, but may be in\n     *     future so should continue to be checked.\n     * NOT_RELEVANT:\n     *     Input is not relevant and will not become relevant, so can\n     *     be ommitted from further checks to optimise performance.\n     */\n    static get relevance() {\n        return {\n            NOT_RELEVANT: 1,\n            POTENTIALLY_RELEVANT: 2,\n            RELEVANT: 3,\n        };\n    }\n\n    /*\n     * relevantInputTypes (string) - a comma separated list of\n     * selectors that describe the types of input that should be\n     * considered relevant.\n     */\n    constructor(relevantInputTypes) {\n        this.relevantInputTypes = relevantInputTypes;\n    }\n\n    /*\n     * Checks whether any of the given input\'s \'labels\' (the ID, name,\n     * or actual label) match the given regex.\n     */\n    static testInputLabel(regex, input) {\n        const labels = [\n            input.id,\n            input.name,\n            InputAnalyser.getLabelText(input),\n        ];\n        return labels.some(label => regex.test(label));\n    }\n}\n\n/*\n * RelevantInput for passwords.\n */\nclass Password extends RelevantInput {\n    constructor() {\n        super(\'input[type=password]\');\n    }\n\n    /*\n     * Passwords are verified purely by their input type.\n     */\n    test(input) {\n        if (input.matches(this.relevantInputTypes)) {\n            return RelevantInput.relevance.RELEVANT;\n        } else {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n    }\n}\n\n/*\n * RelevantInput for card numbers.\n */\nclass CardNumber extends RelevantInput {\n    constructor() {\n        // Inputs with no type attribute act like text inputs\n        super(\n            \'input[type=text], input[type=tel], input[type=number], input:not([type])\'\n        );\n\n        // Definitely card number labels\n        this.inputLabelRegexes = [\n            /(cc|card).?(num|no)/i,\n            /&#12459;&#12540;&#12489;&#30058;&#21495;/,\n            /&#1053;&#1086;&#1084;&#1077;&#1088;.*&#1082;&#1072;&#1088;&#1090;&#1099;/i,\n            /&#20449;&#29992;&#21345;&#21345;&#34399;/i,\n            /&#20449;&#29992;&#21345;&#21495;&#30721;/i,\n        ];\n\n        /*\n         * Matches incomplete card numbers, because ideally the\n         * skimmer should be detected before the victim gives away\n         * most of their card number.\n         */\n        this.inputValueRegexes = [/^(\\d{4}[-\\s]?){0,4}\\d{1,4}$/];\n\n        // Probably card number labels\n        this.suspectInputLabelRegexes = [\n            /card/i,\n            /tarjeta/i,\n            /carte/i,\n            /&#1082;&#1072;&#1088;&#1090;&#1099;/i,\n            /&#21345;/i,\n            /&#12459;&#12540;&#12489;/i,\n            /&#52852;&#46300;/i,\n            /credit[^s]/i,\n        ];\n\n        // If it matches one of these, probably not a card number\n        this.inputLabelExceptionRegexes = [\n            /post/i,\n            /code/i,\n            /zip/i,\n            /acc(oun)?t?[-_]?n(um|o)/i,\n            /credit[-_]?(sum|limit)/i,\n            /loyalty/i,\n            /gift/i,\n            /idcard/i,\n            /cardid/i,\n            /club/i,\n            /sku/i,\n            /(\\b|_)otp/i,\n            /price/i,\n            /amount/i,\n            /service/i,\n            /picard/i,\n            /search/i,\n            /contact/i,\n            /callme/i,\n            /chat/i,\n            /activate/i,\n            /msisdn/i,\n        ];\n    }\n\n    test(input) {\n        if (!input.matches(this.relevantInputTypes)) {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n\n        // Return NOT_RELEVANT if label matches an exception regex\n        if (\n            this.inputLabelExceptionRegexes.some(r =>\n                RelevantInput.testInputLabel(r, input)\n            )\n        ) {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n\n        // Return RELEVANT if label clearly states card number\n        if (\n            this.inputLabelRegexes.some(r =>\n                RelevantInput.testInputLabel(r, input)\n            )\n        ) {\n            return RelevantInput.relevance.RELEVANT;\n        }\n\n        /*\n         * If label suggests card number, return RELEVANT if value\n         * also suggests card number or POTENTIALLY_RELEVANT if\n         * not. Otherwise, return NOT_RELEVANT as this element will\n         * not become relevant unless a label changes (unlikely).\n         */\n        if (\n            this.suspectInputLabelRegexes.some(r =>\n                RelevantInput.testInputLabel(r, input)\n            )\n        ) {\n            if (this.inputValueRegexes.some(r => r.test(input.value))) {\n                return RelevantInput.relevance.RELEVANT;\n            } else {\n                return RelevantInput.relevance.POTENTIALLY_RELEVANT;\n            }\n        } else {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n    }\n}\n\n    const inputAnalyser = new InputAnalyser();\n\n    const resourceBlockList = new Set([\n                            \"6C590F97DDB1A11AE795B3050E4C3866\", \"D285BE79786E2ECE396941331C5BA65E\", \"59DF8523520DE7F08FB53D3D12BD94F5\", \"1FB784EBAFDA1618023756BF5B6D7E46\", \"0D6E1B5F03B81F5C37813B22E29C4FE2\", \"EFAB0ECE9383D50C555B8168C3E2A4DD\", \"AC8A4B13A411A4E43E61D0A034FA4A55\", \"25A781F6D1F94EEACCF1E00E844D9E14\", \"3920B04ED0D215EA738F524F60485F23\", \"05F4A2D3161A92EB82A44F15FC2A3AEC\", \"C5A1BE7B3855B0A25275B2877E005CB9\", \"00069CEC347076721EBE2257978A0A33\", \"9A513130D02F360BBDC16D8AC0C579D8\", \"7CDB2A3A29E50F90C1000252EF523AD6\", \"25CE912FAF2376026AAD0174AD5A0329\", \"739E1F67AA5F64A06FC469F6030802D4\", \"F49368C9A05C29EF49411999C5987F2C\", \"AB2DD89FB892155275D8780CF527BC93\", \"CFE758EB18FBFE70B1F690241B75D2B1\", \"E43C3463B40A790545F4149FF06B419E\", \"8B6D4C76A7CCF5EB720660B8B2D3F5EB\", \"45573B3E2F613AF5BF2145DF004BABD1\", \"E3DB4E968B371DE94CE607199A062F39\", \"A4EFF1240C5DE614B394FB738CBD28E2\", \"F42D10E84B9A69A8A24062319F425A83\", \"90646BE39982594A58EDF5D2630CCFFD\", \"669853BEA9B84EA51D7302A7FBFF5BC3\", \"0BA566B4777526F451108100107DC5ED\", \"AB19E12BFFBC47244BDCC2403EAA9037\", \"61DAF8537240639277FD1A5908A131A4\", \"6F1DC1309CFE734C038685D8F17A3B6B\", \"DCC3DB001334F29C1C0208D15E9EAB49\", \"2DBD7D54022C28273ADE3147B8EEB076\", \"AE3760E93412C65EC5BF63537B037019\", \"4CD96A830DAC0E0B5EBE578B0DD870AE\", \"4C70569BBE1821F5655382660807FA77\", \"05ECA17FF0EA78669ED43B2BD7B63571\", \"44E503B3621513B9D3666F284E4DF47A\", \"8F1C92E7665860AE70CBB228DED86111\", \"D69552885B55C4097E5725F805815639\", \"B4357A9D889E974CDF3F9740E48F9A1B\", \"5709DC60CFC365B7064B06C709A9D32B\", \"14E8729CBB16AC3625EAA0D7A679E56E\", \"793D4DABAAA377A12F2B6CE88C5FB389\", \"C5D1DCE2AE0D8CE79054FC16CB13D65A\", \"89F91750BD4A9397D1F8CE183757FF70\", \"50B8BC213FB188A687173C1A0A77EA57\"\n                        ]);\n    const evilJSPatterns = new Set([\n                            \n                        ]);\n\n    // Extract body object\n    function extractBody(data) {\n        if (!data) {\n            return Promise.resolve({});\n        }\n        if (FormData.prototype.isPrototypeOf(data)) {\n            const formData = {};\n            for (const [key, value] of data.entries()) {\n                formData[key] = value;\n            }\n            return Promise.resolve({ formData });\n        }\n        // Create a temporary Request object to parse the body\n        const tempRequest = new Request(\'\', { body: data, method: \'POST\' });\n        return extractBodyFromRequest(tempRequest);\n    }\n\n    function extractBodyFromRequest(request) {\n        // Try to extract as text\n        return request\n            .text()\n            .then(text => {\n                return { text };\n            })\n            .catch(e => {\n                return {};\n            });\n    }\n\n    /**\n     * This function checks if the request hostname/URL is in the blocklist.\n     * Used when identifying whether or not to forward request to background\n     * script.\n     * @param hostname Hostname to check against blocklist.\n     * @param url Request url to check against evil JS patterns\n     * @returns boolean true if hostname is in the blocklist, false if\n     * it is not in blocklist or if blocklist is not provided.\n     */\n    function requestInBlocklist(hostname, url) {\n        if (resourceBlockList === undefined || evilJSPatterns === undefined) {\n            return false;\n        }\n        if (testEvilJS(url.replace(/^(https?:)?\\/\\//i, \'\'))) {\n            return true;\n        }\n        while (!testEvilJS(hostname) && !testResource(hostname)) {\n            const newHostname = hostname.replace(/^.+?\\./g, \'\');\n\n            if (hostname === newHostname) return false;\n\n            hostname = newHostname;\n        }\n        return true;\n    }\n\n    function testEvilJS(url) {\n        return evilJSPatterns.has(base64hash(sha512, url));\n    }\n\n    function testResource(hostname) {\n        return resourceBlockList.has(\n            md5(cons.evilResourcesSalt + hostname).toUpperCase()\n        );\n    }\n\n    /*\n     * Implementations to overwrite window.XMLHttpRequest.prototype.open,\n     * window.XMLHttpRequest.prototype.send and window.fetch. These are (to my knowledge) the two\n     * APIs that can be used to make requests within Javascript. If there are more, then they will\n     * also need to be overridden and intercepted like below.\n     */\n\n    /**\n     * This function generates functions to be used for wrapping and intercepting network\n     * requests, namely from XMLHttpRequest and the Fetch API. It works by taking the real\n     * function we want to intercept the request for, and then uses document.currentScript to work\n     * out which file initiated the request. It sends those details to the content script which\n     * forwards it to the background script, and then we await a confirmation of receipt from the\n     * background page. Finally, the real request is sent by forwarding parameters to the original\n     * function passed in.\n     * @param realFn A reference to the real function that is being intercepted.\n     * @param getRequestUrlFn A function used to get the request URL from the object. This is\n     * necessary as the mechanisms for different objects differs depending on implementation.\n     * @param getRequestBodyPr A function used to get the request body from the object. Returns\n     * a promise. It should be null if it\'s not applicable (e.g. if it sets the src of an image or script).\n     * @param type The type of request, which will be a ResourceType:\n     * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType\n     * @returns {function(...[*]=): PromiseLike<any | never>}\n     */\n    function generateRequestInterceptFn(\n        realFn,\n        getRequestUrlFn,\n        getRequestBodyPr,\n        type\n    ) {\n        const patched = function(...args) {\n            /*\n             * We create a link DOM element in order to correctly generate the\n             * absolute path of the request, automatically transforming from a\n             * relative path if necessary.\n             */\n            const requestUrl = normaliseUrl(\n                getRequestUrlFn.bind(this)(...args)\n            );\n\n            let requestHostname;\n            let pageHostname;\n\n            try {\n                requestHostname = new URL(requestUrl).hostname;\n                pageHostname = new URL(location.href).hostname;\n            } catch (e) {\n                return realFn.apply(this, args);\n            }\n\n            /*\n             * If hostname is in the blocklist, the request should always be\n             * sent to the background script.\n             */\n            // eslint-disable-next-line prefer-const\n            let forceStall = false;\n\n            if (!forceStall) {\n                /*\n                 * Delaying outgoing requests sometimes causes problems\n                 * with elements such as video players and dynamically\n                 * loaded images, so it should only be done when\n                 * necessary. Before inputAnalyser is defined and when the\n                 * request is to the same hostname as the page, this can\n                 * be skipped.\n                 */\n                if (\n                    requestHostname === pageHostname ||\n                    inputAnalyser === undefined\n                ) {\n                    return realFn.apply(this, args);\n                }\n\n                /*\n                 * It can also be skipped if there is no sensitive\n                 * information entered into a form on the page yet.\n                 */\n                const relevantFilledInputs = inputAnalyser.relevantFilledInputs;\n                if (!relevantFilledInputs.length) {\n                    return realFn.apply(this, args);\n                }\n\n                /*\n                 * For requests with no body, it\'s only necessary to delay\n                 * them when the sensitive information from the form\n                 * inputs is present in the request URL.\n                 */\n                const ia = new InputAnalyser(relevantFilledInputs);\n                const relevantInputValues = ia.relevantInputValues;\n                const urlMatcher = new URLParamsMatcher(requestUrl);\n\n                if (\n                    getRequestBodyPr === null &&\n                    !urlMatcher.findInputMatches(relevantInputValues).length\n                ) {\n                    return realFn.apply(this, args);\n                }\n            }\n\n            /*\n             * Create a promise that resolves immediately if the request body\n             * does not apply to this request.\n             */\n            if (getRequestBodyPr === null) {\n                getRequestBodyPr = function() {\n                    return Promise.resolve({});\n                };\n            }\n\n            let fileName;\n            if (document.currentScript && document.currentScript.src) {\n                fileName = document.currentScript.src;\n            } else if (callbackCurrentScript && callbackCurrentScript.src) {\n                fileName = callbackCurrentScript.src;\n            } else {\n                fileName = location.href;\n            }\n\n            return getRequestBodyPr\n                .bind(this)(...args)\n                .then(requestBody => {\n                    /*\n                     * Generate the information object to send through to the\n                     * content script including the request URL, the initiator\n                     * filename and a timestamp relative to the page load.\n                     */\n                    const detail = {\n                        fileName,\n                        requestBody,\n                        requestUrl,\n                        timestamp: performance.now(),\n                        type,\n                    };\n\n                    document.dispatchEvent(\n                        new CustomEvent(cons.capturedIntercept, {\n                            detail,\n                        })\n                    );\n\n                    return new Promise(resolve => {\n                        /*\n                         * Send the initiator\'s file name and other info to the\n                         * background script, then wait for confirmation that the\n                         * details made it to the background page before calling\n                         * the real function. This is to avoid any race conditions\n                         * whereby the details haven\'t been added to the tab data\n                         * before onBeforeRequest/onBeforeSendHeaders gets called.\n                         */\n                        document.addEventListener(\n                            cons.confirmedIntercept,\n                            function compareData(customEvent) {\n                                const receivedData = JSON.parse(\n                                    customEvent.detail\n                                );\n                                if (detailsMatch(detail, receivedData)) {\n                                    /*\n                                     * If it\'s the correct confirmation, then we\n                                     * can resolve the promise and call the real\n                                     * function in the finally block.\n                                     */\n                                    document.removeEventListener(\n                                        cons.confirmedIntercept,\n                                        compareData,\n                                        false\n                                    );\n                                    resolve();\n                                }\n                            }\n                        );\n                    }).then(() => realFn.apply(this, args));\n                });\n        };\n        // make toString() look native, #34\n        patched.toString = () => realFn.toString();\n        return patched;\n    }\n\n    function detailsMatch(first, second) {\n        const comparisonKeys = [\'fileName\', \'requestUrl\', \'timestamp\', \'type\'];\n        for (const key of comparisonKeys) {\n            if (first[key] !== second[key]) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    window.fetch = generateRequestInterceptFn(\n        realFetch,\n        request => (typeof request === \'string\' ? request : request.url),\n        (request, options) => {\n            // Check if argument is a Request object.\n            if (request instanceof Request) {\n                return extractBodyFromRequest(request);\n            }\n            return options ? extractBody(options.body) : Promise.resolve({});\n        },\n        \'xmlhttprequest\'\n    );\n\n    window.XMLHttpRequest.prototype.send = generateRequestInterceptFn(\n        realSend,\n        function getRequestUrl() {\n            return this._requestUrl;\n        },\n        extractBody,\n        \'xmlhttprequest\'\n    );\n\n    /*\n     * This function is intercepted so that we can capture the value of the\n     * request URL, since it isn\'t present in the arguments for\n     * XMLHttpRequest.send (above).\n     */\n    window.XMLHttpRequest.prototype.open = function open(...args) {\n        [, this._requestUrl] = args;\n        realOpen.apply(this, args);\n    };\n    window.XMLHttpRequest.prototype.open.toString = () => realOpen.toString();\n\n    /*\n     * Monkeypatches for setting the src of images and scripts, an\n     * indirect way of sending requests that is often employed by\n     * skimmers. The getters are also overridden to make the behaviour\n     * seem identical to the original implementation. Otherwise, if a\n     * script were to set an src and immediately read it, it would\n     * come back as the old value.\n     */\n\n    /*\n     * WeakMap maps an object to a value. This is used to store the\n     * new src of an object when its true src property may not yet\n     * have been set.\n     */\n    const srcMap = new WeakMap();\n    const requestElements = [\n        {\n            srcElement: HTMLImageElement.prototype,\n            type: \'image\',\n        },\n        {\n            srcElement: HTMLScriptElement.prototype,\n            type: \'script\',\n        },\n    ];\n\n    for (const { srcElement, type } of requestElements) {\n        const realGetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\')\n            .get;\n        const realSetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\')\n            .set;\n        const realGetAttribute = srcElement.getAttribute;\n        const realSetAttribute = srcElement.setAttribute;\n        const realRemoveAttribute = srcElement.removeAttribute;\n\n        const interceptFn = generateRequestInterceptFn(\n            realSetSrc,\n            src => `${src}`,\n            null,\n            type\n        );\n\n        function setSrc(url) {\n            /*\n             * It\'s valid to provide a TrustedScriptURL wrapping the URL but we need to\n             * return a string in the getter, so we convert the url to a string here.\n             *\n             * The url can also be undefined/null so we can\'t just call `.toString()`.\n             *\n             * See #56.\n             */\n            srcMap.set(this, `${url}`);\n            interceptFn.call(this, url);\n            return url;\n        }\n\n        function getSrc() {\n            if (srcMap.get(this) === undefined) {\n                return realGetSrc.call(this);\n            } else {\n                return normaliseUrl(srcMap.get(this));\n            }\n        }\n\n        getSrc.toString = () => realGetSrc.toString();\n        setSrc.toString = () => realSetSrc.toString();\n\n        Object.defineProperty(srcElement, \'src\', {\n            get: getSrc,\n            set: setSrc,\n        });\n\n        srcElement.setAttribute = function(attributeName, value) {\n            if (attributeName.toUpperCase() === \'SRC\') {\n                setSrc.call(this, value);\n            } else {\n                realSetAttribute.call(this, attributeName, value);\n            }\n        };\n        srcElement.setAttribute.toString = () => realSetAttribute.toString();\n\n        srcElement.getAttribute = function(attributeName) {\n            if (\n                attributeName.toUpperCase() === \'SRC\' &&\n                srcMap.get(this) !== undefined\n            ) {\n                return srcMap.get(this);\n            } else {\n                return realGetAttribute.call(this, attributeName);\n            }\n        };\n        srcElement.getAttribute.toString = () => realGetAttribute.toString();\n\n        srcElement.removeAttribute = function(attributeName) {\n            if (attributeName.toUpperCase() === \'SRC\') {\n                srcMap.delete(this);\n            }\n\n            return realRemoveAttribute.call(this, attributeName);\n        };\n        srcElement.removeAttribute.toString = () =>\n            realRemoveAttribute.toString();\n    }\n})(\n    window.XMLHttpRequest.prototype.open,\n    window.XMLHttpRequest.prototype.send,\n    window.fetch\n);\n</script><script>/*\n * This entire block is wrapped in an IIFE to prevent polluting the scope of the web page with\n * functions created by this extension.\n */\n(function(realOpen, realSend, realFetch) {\n    /*\n     * Sometimes pages overwrite standard objects with global\n     * variables. Where possible, we extract fresh versions from an\n     * iframe.\n     */\n    const iframe = document.createElement(\'iframe\');\n    document.documentElement.appendChild(iframe);\n\n    // Initialise with global values\n    let URL = window.URL;\n    let WeakMap = window.WeakMap;\n    let WeakSet = window.WeakSet;\n    let TextDecoder = window.TextDecoder;\n    let Uint8Array = window.Uint8Array;\n    let decodeURIComponent = window.decodeURIComponent;\n    let URLSearchParams = window.URLSearchParams;\n    let Array = window.Array;\n    let Object = window.Object;\n    let atob = window.atob;\n    let CustomEvent = window.CustomEvent;\n    let performance = window.performance;\n    let JSON = window.JSON;\n\n    /*\n     * Under some circumstances the iframe isn\'t added\n     * successfully. If it has a contentWindow then we can extract the\n     * values from it, but otherwise we fall back on the global\n     * values.\n     */\n    if (iframe.contentWindow) {\n        // Try block because sometimes the CORS prevents access to iframe\n        try {\n            URL = iframe.contentWindow.URL;\n            WeakMap = iframe.contentWindow.WeakMap;\n            WeakSet = iframe.contentWindow.WeakSet;\n            TextDecoder = iframe.contentWindow.TextDecoder;\n            Uint8Array = iframe.contentWindow.Uint8Array;\n            decodeURIComponent = iframe.contentWindow.decodeURIComponent;\n            URLSearchParams = iframe.contentWindow.URLSearchParams;\n            Array = iframe.contentWindow.Array;\n            Object = iframe.contentWindow.Object;\n            atob = iframe.contentWindow.atob;\n            CustomEvent = iframe.contentWindow.CustomEvent;\n            performance = iframe.contentWindow.performance;\n            JSON = iframe.contentWindow.JSON;\n        } catch (error) {\n            // Empty catch block to prevent errors from showing in page logs\n        }\n    }\n\n    // If iframe was added successfully, remove it\n    if (iframe.parentNode) {\n        iframe.parentNode.removeChild(iframe);\n    }\n\n    /*\n     * If any of the values don\'t have the correct type, return and\n     * don\'t attempt any monkey patching. This avoids breaking web\n     * pages.\n     */\n    for (f of [\n        URL,\n        WeakMap,\n        WeakSet,\n        TextDecoder,\n        Uint8Array,\n        decodeURIComponent,\n        URLSearchParams,\n        Array,\n        Object,\n        atob,\n        CustomEvent,\n    ]) {\n        if (typeof f !== \'function\') {\n            return;\n        }\n    }\n\n    for (o of [performance, JSON]) {\n        if (typeof o !== \'object\') {\n            return;\n        }\n    }\n\n    /*\n     * Override common funcions that accept callbacks, such as\n     * setInterval, to keep track of what script each callback comes\n     * from. Skimmers commonly have the actual skimming code in a\n     * callback, so most of the time when we intercept a request,\n     * document.currentScript is null. To solve this, the script is\n     * stored in a closure when the callback is passed, and assigned\n     * to callbackCurrentscript when the callback is run.\n     */\n\n    let callbackCurrentScript = null;\n\n    // Maps callbacks originally passed to patched versions\n    const patchedCallbackMap = new WeakMap();\n\n    // Maps patched callbacks to original versions\n    const originalCallbackMap = new WeakMap();\n\n    /**\n     *  Generates the function with which to override the\n     *  callback-accepting functions.\n     *  @param realFn The real function, e.g. setInterval.\n     *  @param callbackIndex The index of the callback in the\n     *  arguments of realFn. For example, the callback is the first\n     *  argument in setInterval, so callbackIndex should be 0.\n     */\n    function generateAddCallbackFunction(realFn, callbackIndex) {\n        const patched = function(...args) {\n            const script = document.currentScript || callbackCurrentScript;\n\n            const callback = args[callbackIndex];\n\n            // If the callback isn\'t a function, there\'s nothing to patch\n            if (typeof callback !== \'function\') {\n                return realFn.apply(this, args);\n            }\n\n            // If a patch exists, use it instead of creating new function\n            let patchedCallback = patchedCallbackMap.get(callback);\n            if (!patchedCallback) {\n                patchedCallback = function() {\n                    callbackCurrentScript = script;\n                    const retVal = callback.apply(this, [...arguments]);\n                    callbackCurrentScript = null;\n                    return retVal;\n                };\n                patchedCallbackMap.set(callback, patchedCallback);\n                originalCallbackMap.set(patchedCallback, callback);\n            }\n\n            args[callbackIndex] = patchedCallback;\n            return realFn.apply(this, args);\n        };\n        // make toString() look native, #34\n        patched.toString = () => realFn.toString();\n        return patched;\n    }\n\n    const realSetInterval = window.setInterval;\n\n    window.setInterval = generateAddCallbackFunction(realSetInterval, 0);\n\n    const realSetTimeout = window.setTimeout;\n\n    window.setTimeout = generateAddCallbackFunction(realSetTimeout, 0);\n\n    const realPromiseThen = Promise.prototype.then;\n    Promise.prototype.then = generateAddCallbackFunction(realPromiseThen, 0);\n\n    const realPromiseCatch = Promise.prototype.catch;\n    Promise.prototype.catch = generateAddCallbackFunction(realPromiseCatch, 0);\n\n    const realPromiseFinally = Promise.prototype.finally;\n    if (realPromiseFinally) {\n        Promise.prototype.finally = generateAddCallbackFunction(\n            realPromiseFinally,\n            0\n        );\n    }\n\n    const realAddEventListener = EventTarget.prototype.addEventListener;\n\n    EventTarget.prototype.addEventListener = generateAddCallbackFunction(\n        realAddEventListener,\n        1\n    );\n\n    /*\n     * removeEventListener needs to be patched to map the passed\n     * listener to the patched listener.\n     */\n    const realRemoveEventListener = EventTarget.prototype.removeEventListener;\n    EventTarget.prototype.removeEventListener = function(...args) {\n        args[1] = patchedCallbackMap.get(args[1]) || args[1];\n\n        return realRemoveEventListener.apply(this, args);\n    };\n    EventTarget.prototype.removeEventListener.toString = () =>\n        realRemoveEventListener.toString();\n\n    /*\n     * Event listeners are sometimes set by directly assigning to a\n     * property from the GlobalEventHandlers mixin,\n     * e.g. \'submitBtn.onclick\'. For each of the GlobalEventHandlers\n     * implementations that are likely to be relevant, the event\n     * properties likely to be used by skimmers are overridden with\n     * our version that tracks currentScript when assigned.\n     */\n    for (const eventTarget of [\n        HTMLElement.prototype,\n        Document.prototype,\n        window,\n    ]) {\n        for (const eventName of [\n            \'onclick\',\n            \'onblur\',\n            \'onfocus\',\n            \'onchange\',\n            \'oninput\',\n            \'onkeydown\',\n            \'onkeypress\',\n            \'onkeyup\',\n            \'onload\',\n            \'onloadend\',\n            \'onmousedown\',\n            \'onmouseenter\',\n            \'onmouseleave\',\n            \'onmousemove\',\n            \'onmouseout\',\n            \'onmouseover\',\n            \'onmouseup\',\n            \'onpointerdown\',\n            \'onpointermove\',\n            \'onpointerup\',\n            \'onpointerover\',\n            \'onpointerout\',\n            \'onpointerenter\',\n            \'onpointerleave\',\n            \'onsubmit\',\n        ]) {\n            const descriptor = Object.getOwnPropertyDescriptor(\n                eventTarget,\n                eventName\n            );\n\n            if (!descriptor) {\n                continue;\n            }\n\n            const realGetter = descriptor.get;\n            const realSetter = descriptor.set;\n\n            const patchedSetter = generateAddCallbackFunction(realSetter, 0);\n\n            /*\n             * In case a script checks the value of the property after\n             * setting it, the getter has to be patched to return the\n             * original callback.\n             */\n            const patchedGetter = function() {\n                const realValue = realGetter.call(this);\n\n                const originalCallback = originalCallbackMap.get(realValue);\n                if (originalCallback) {\n                    return originalCallback;\n                } else {\n                    return realValue;\n                }\n            };\n\n            patchedGetter.toString = () => realGetter.toString();\n\n            Object.defineProperty(eventTarget, eventName, {\n                get: patchedGetter,\n                set: patchedSetter,\n            });\n        }\n    }\n\n    /*\n     * This script has some dependencies that need to be included\n     * without polluting the global namespace. This is achieved by\n     * inserting them into the source at build time in the\n     * makefile. This is preferable to loading the dependencies via\n     * AJAX, because the latter doesn\'t work if the CSP disallows\n     * unsafe-eval. The following tag tells the makefile where to\n     * insert the dependencies.\n     */\n    // <DEPENDENCIES>\nclass InputAnalyser {\n    constructor(_inputs) {\n        this._inputs = _inputs;\n        this._irrelevantInputs = new WeakSet();\n\n        /*\n         * Not currently using Password because it was causing too\n         * many false positives. Some sites use external auth domains,\n         * and many have misconfigured analytics that collect\n         * passwords.\n         */\n        this.relevantInputs = [new CardNumber()];\n    }\n\n    get relevantInputValues() {\n        const inputs = {};\n        this._inputs.forEach(\n            input => (inputs[input.id || input.name] = input.value)\n        );\n\n        return inputs;\n    }\n\n    get relevantFilledInputs() {\n        return this.allInputs\n            .filter(input => this.isRelevantElement(input))\n            .filter(input => input.value.length >= cons.minInputSize)\n            .map(input => ({\n                id: input.id,\n                name: input.name,\n                value: input.value,\n            }));\n    }\n\n    get allInputs() {\n        this._inputs = [\n            ...document.querySelectorAll(\n                this.relevantInputs.map(i => i.relevantInputTypes).join(\', \')\n            ),\n        ];\n\n        return this._inputs;\n    }\n\n    static getLabelText(input) {\n        if (!input.labels.length) {\n            return \'\';\n        }\n        return stripSpecialChars(input.labels[0].textContent).trim();\n    }\n\n    /**\n     * Takes an HTMLElement and determines whether or not it could be relevant for the purposes of\n     * skimmer detection.\n     * @param {HTMLElement} input The element which is to be checked for relevancy.\n     * @returns {boolean} Returns true if the element is potentially relevant (e.g. a credit card\n     * field or other such sensitive input).\n     */\n    isRelevantElement(input) {\n        // If the element has been identified as irrelevant, return false\n        if (this._irrelevantInputs.has(input)) {\n            return false;\n        }\n\n        // Check the input against all the relevant input types\n        const relevances = this.relevantInputs.map(i => i.test(input));\n\n        /*\n         *  If they all agree that this input is unlikely to become\n         *  relevant, exclude it from further checks and return false\n         */\n        if (relevances.every(i => i === RelevantInput.relevance.NOT_RELEVANT)) {\n            this._irrelevantInputs.add(input);\n            return false;\n        }\n\n        // Return true if any consider the input to be relevant, otherwise false\n        return relevances.some(i => i === RelevantInput.relevance.RELEVANT);\n    }\n}\n/*\n * Input Matcher represents a class that can have matches with relevant inputs.\n * Each class that inherits must implement the findInputMatches method.\n */\nclass InputMatcher {\n    /*\n     * Finds any matches with the list of inputs.\n     * @param inputs. An object whose values represent relevant inputs.\n     * @return {[String]} The keys of the inputs that were found.\n     */\n    findInputMatches(inputs) {\n        throw new Error(\'findInputMatches has not been implemented.\');\n    }\n\n    /**\n     * Determines whether one or more values in needles are present in any of\n     * the keys of the haystacks.\n     * Checks both raw input and base64 encoded versions of the needles.\n     * @param needles An object. The haystacks are searched for the values of\n     * this object.\n     * @param haystacks The array of strings in which the needle(s) may reside.\n     * A string may optionally be supplied if only one value is present.\n     * @returns {[String]} The keys of the needles that were found in one or\n     * more haystacks in either raw or base64 encoded form.\n     */\n    static atLeastOneNeedleInHaystack(needles, haystacks) {\n        if (\n            needles === undefined ||\n            needles.length === 0 ||\n            haystacks === undefined\n        ) {\n            return [];\n        }\n\n        const searchArray = Array.isArray(haystacks) ? haystacks : [haystacks];\n\n        return Object.entries(needles)\n            .filter(([_, needle]) =>\n                searchArray.some(haystack => {\n                    if (isBase64Encoded(haystack)) {\n                        if (atob(haystack).indexOf(needle) !== -1) {\n                            return true;\n                        }\n                    }\n                    return haystack.indexOf(needle) !== -1;\n                })\n            )\n            .map(([input, _]) => input);\n    }\n}\n/*\n * Matcher for an object that cannot contain any inputs.\n */\nclass EmptyMatcher extends InputMatcher {\n    findInputMatches(inputs) {\n        return [];\n    }\n}\n\nclass URLParamsMatcher extends InputMatcher {\n    constructor(url) {\n        super();\n        this._url = url;\n    }\n\n    findInputMatches(inputs) {\n        try {\n            const parsedUrl = new URL(this._url);\n            return InputMatcher.atLeastOneNeedleInHaystack(inputs, [\n                ...parsedUrl.searchParams.values(),\n            ]);\n        } catch (e) {\n            return [];\n        }\n    }\n}\n\nclass StringBodyMatcher extends InputMatcher {\n    constructor(str) {\n        super();\n        this._str = str;\n    }\n\n    findInputMatches(inputs) {\n        let decoded;\n        try {\n            decoded = decodeURIComponent(this._str);\n        } catch (e) {\n            decoded = this._str;\n        }\n\n        const searchParams = new URLSearchParams(decoded);\n        const parsedQuery = {};\n        const parsedValues = [];\n\n        for (const [key, value] of searchParams.entries()) {\n            parsedQuery[key] = value;\n            parsedValues.push(value);\n        }\n\n        if (parsedQuery !== {} && parsedValues.indexOf(null) === -1) {\n            return InputMatcher.atLeastOneNeedleInHaystack(\n                inputs,\n                parsedValues\n            );\n        }\n        return InputMatcher.atLeastOneNeedleInHaystack(inputs, decoded);\n    }\n}\n\nclass FormBodyMatcher extends InputMatcher {\n    constructor(formData) {\n        super();\n        this._formData = formData;\n    }\n\n    findInputMatches(inputs) {\n        return InputMatcher.atLeastOneNeedleInHaystack(\n            inputs,\n            Object.values(this._formData).flat()\n        );\n    }\n}\n/**\r\n * Takes a string and removes a variety of \'special\' characters like punctuation.\r\n * @param str The string to be modified.\r\n * @returns {string} The resulting string without any special characters.\r\n */\r\nfunction stripSpecialChars(str) {\r\n    return str.replace(/[`~!@#$%^&*()_|+\\-=?;:\'\",.<>}{\\][\\\\/]/gi, \'\');\r\n}\r\n\r\n/**\r\n * Attempts to identify where a string is base-64 encoded or not.\r\n * @param string The raw string to be examined.\r\n * @returns {boolean} Returns true if the string matches a base-64 string. False otherwise.\r\n */\r\nfunction isBase64Encoded(string) {\r\n    return /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/.test(\r\n        string\r\n    );\r\n}\r\n\r\n// Computes the base64 sha512/sha224 hash of some text, removing trailing ==\r\nfunction base64hash(hashFunction, text) {\r\n    return btoa(\r\n        hashFunction\r\n            .array(text)\r\n            .map(char => String.fromCharCode(char))\r\n            .join(\'\')\r\n    ).slice(0, -2);\r\n}\r\n\r\nfunction normaliseUrl(str) {\r\n    const link = document.createElement(\'a\');\r\n    link.href = str;\r\n    return link.href;\r\n}\r\n\r\nfunction stripCredentials(urlstr) {\r\n    try {\r\n        const url = new URL(urlstr);\r\n        const params = url.searchParams;\r\n        params.forEach((_, key) => params.set(key, \'\'));\r\n        return url.toString();\r\n    } catch (e) {\r\n        return urlstr;\r\n    }\r\n}\r\n\r\n/**\r\n * Check if two URLs match regardless of any partial encoding.\r\n *\r\n * Uses decodeURI which can throw a URIError when url is malformed.\r\n */\r\nfunction urlsMatch(first, second) {\r\n    try {\r\n        return decodeURI(first) === decodeURI(second);\r\n    } catch (e) {\r\n        return false;\r\n    }\r\n}\r\n\r\nfunction sendMessage(type, data, tabId, options) {\r\n    if (tabId) {\r\n        chrome.tabs.sendMessage(tabId, { data, type }, options);\r\n    } else {\r\n        chrome.runtime.sendMessage({ data, type });\r\n    }\r\n}\r\n\r\n// Encodes the given input using base64url as defined by RFC4648 section 5.\r\nfunction base64Url(input) {\r\n    return btoa(input)\r\n        .replace(\'+\', \'-\')\r\n        .replace(\'/\', \'_\');\r\n}\r\n/*\n * Netcraft Extension\n * constants JavaScript\n */\n\n/* eslint sort-keys: \'off\' */\n/* sort-keys is disabled as the constants are separated by type */\n\n// Aligned values here make for easier reading, so we disable prettier\n\n// prettier-ignore\nconst cons = (function() {\n    const cryptojackersUrl = \'https://www.netcraft.com/apps/help/glossary/#crypto-miners\';\n    const skimmersUrl = \'https://www.netcraft.com/apps/help/glossary/#credit-card-skimmers\';\n    const evilResourceCrime = \'attempted to load\';\n\n    return {\n        fields: [\n            \'topsites\',\n            \'country\',\n            \'netblock\',\n            \'hoster\',\n            \'firstseen\',\n            \'rank\',\n            \'risk\',\n            \'uses_sslv3\',\n            \'pfs\',\n            \'heartbleed\',\n            \'heartbleed_message\',\n        ],\n\n        // options that can be toggle in options.js and installed flag\n\n        toggleOptions: [\n            \'installed\',\n            \'block\',\n            \'xss\',\n            \'suspicious\',\n            \'other-malicious-scripts\',\n            \'credential-leaks\',\n            \'analytics\',\n            \'cryptojackers\',\n            \'skimmers\',\n        ],\n\n        // domains\n        mirror:  \'https://mirror2.extension.netcraft.com/\',\n        toolbar: \'https://mirror.toolbar.netcraft.com/\',\n\n        // links and faqs\n        blogArchive:      \'https://news.netcraft.com/archives/\',\n        browserApp:       \'https://www.netcraft.com/apps/browser/\',\n        cryptojackersUrl,\n        extensionStore:   \'https://addons.mozilla.org/en-us/firefox/addon/netcraft-toolbar?src=external-blocked_visit-rating\',\n        heartbleedUrl:    \'https://www.netcraft.com/apps/help/glossary/#heartbleed\',\n        logBlockedVisit:  \'https://toolbar.netcraft.com/blocked_visit\',\n        netblock:         \'https://sitereport.netcraft.com/netblock?q=\',\n        pfsUrl:           \'https://www.netcraft.com/apps/help/glossary/#pfs\',\n        report:           \'https://sitereport.netcraft.com/?url=\',\n        reportAPI:        \'https://report.netcraft.com/api/v2/report/urls\',\n        reportMistake:    \'https://report.netcraft.com/report/mistake?url=\',\n        skimmersUrl,\n        sslv3Url:         \'https://www.netcraft.com/apps/help/glossary/#sslv3\',\n        submissionURL:    \'https://report.netcraft.com/submission/\',\n        topsites:         \'https://trends.netcraft.com/topsites?s=\',\n\n        // paths\n        block:              \'blocked.html\',\n        check:              \'check_url/v4/\',\n        danger:             \'Images/danger.png\',\n        embed:              \'/embedded\',\n        evilJSPatternsFeed: \'eviljspatterns\',\n        skimmerDomainWhitelistFeed: \'eviljsskimmerwhitelist\',\n        evilResourcesFeed:  \'blockdb/netcraft_extension_evil_resource_encrypted-yppL7bVh/\',\n        flags:              \'Images/flags/\',\n        force:              \'/dodns\',\n        heartbleed:         \'Images/heartbleed.png\',\n        info:               \'/info\',\n        poodlePost:         \'2014/10/15/googles-poodle-affects-oodles.html\',\n        warning:            \'Images/warning.png\',\n\n        // feed constants\n        evilResourceTypes: [\'cryptojacker\', \'skimmer\'],\n        evilResourcesSalt: \'C4jhYHxW\',\n        maxRevisions:      5,\n        randomSaltLength:  8,\n\n        // mappings of url_type => information\n        urlTypeInformationMappings: {\n            \'malicious_javascript/cryptojacker\': [\'web miners\',             cryptojackersUrl],\n            \'malicious_javascript/skimmer\':      [\'shopping site skimmers\', skimmersUrl],\n        },\n\n        // mappings of evilResourceTypes => user options\n        evilResourceOptionMappings: {\n            \'cryptojacker\': \'cryptojackers\',\n            \'skimmer\':      \'skimmers\'\n        },\n\n        // mappings of user options => url_type\n        optionUrlTypeMappings: {\n            \'block\':                   /^(?!malicious_javascript:)/,\n            \'cryptojackers\':           /^malicious_javascript\\/cryptojacker$/,\n            \'other-malicious-scripts\': /^malicious_javascript\\/(?!(skimmer|cryptojacker)$)/,\n            \'skimmers\':                /^malicious_javascript\\/skimmer$/,\n        },\n\n        // report constants\n        malwareSourceEmail:  \'extension-reported-malware@netcraft.com\',\n        malwareSourceUUID:   \'mwcCcApi89cobY9loJ9rWJ9HuOhlpwMj\',\n        extensionSourceUUID: \'qBEQ3NhEnwK4sypmuSxBJzRqpxZLRXxe\',\n\n        // report error default message\n        reportErrorMessage: \'An error occured with your submission. Please try again.\',\n\n        // mapping from report API errors to displayed messages\n        reportErrorMappings: {\n            \'Duplicate of a recent submission\': \'This URL has already been reported.\',\n            \'Supplied email is not valid\': \'Invalid email address. Please correct it and try again.\',\n        },\n\n        whitelistErrorMessages: {\n            invalidHostname: \'Invalid hostname, please correct it and try again\',\n            duplicate: \'Protection is already disabled for this site\',\n        },\n\n        // browserAction icon paths\n        iconNormal: {\n            16: \'Images/action-normal-16.png\',\n            24: \'Images/action-normal-24.png\',\n            32: \'Images/action-normal-32.png\'\n        },\n        iconDanger: {\n            16: \'Images/action-danger-16.png\',\n            24: \'Images/action-danger-24.png\',\n            32: \'Images/action-danger-32.png\'\n        },\n        iconWarning: {\n            16: \'Images/action-warning-16.png\',\n            24: \'Images/action-warning-24.png\',\n            32: \'Images/action-warning-32.png\'\n        },\n        iconDisabled: {\n            16: \'Images/action-disabled-16.png\',\n            24: \'Images/action-disabled-24.png\',\n            32: \'Images/action-disabled-32.png\'\n        },\n\n        // Extension internal message types\n        capturedIntercept:  \'interceptDetailsCaptured\',\n        confirmedIntercept: \'interceptDetailsConfirmed\',\n        contentReady:       \'contentScriptReady\',\n        leaksEnabled:       \'credentialLeaksEnabled\',\n        reportURL:          \'report\',\n        reqBlock:           \'block\',\n        reqDetails:         \'details\',\n        reqForce:           \'force\',\n        reqPopUp:           \'popup\',\n        reqPrintTimingData: \'time\', //debug\n        reqInputs:          \'requestInputValues\',\n        sendInputs:         \'sendInputs\',\n        sendIntercept:      \'sendInterceptDetails\',\n        updateOption:       \'updateOption\',\n        updateWhitelist:    \'whitelist\', // disable button whitelist\n\n        // Status codes\n        errEvilResource: \'EVILRESOURCE\',\n        errNoDetails:    \'NODETAILS\',\n        errNoInfo:       \'NOINFO\',\n        errBlocked:      \'BLOCKED\',\n        errPending:      \'PENDING\',\n        errServer:       \'ERROR\',\n        errSuspicious:   \'SUSPICIOUS\',\n        errTimeout:      \'TIMEOUT\',\n        errXSS:          \'XSS\',\n        noError:         \'OK\',\n\n        // Valid protocols (it is a regex match so ^ matches start of string)\n        protocols:    [\'^http\', \'^https\', \'^ftp\'],\n\n        // Human readable messages\n        msgNa:           \'NA\',\n        msgNewSite:      \'New Site\',\n        msgNoInfo:       \'Site information not available\',\n        msgNoServer:     \'Unable to contact Netcraft servers\',\n        msgSuspicious:   \'Suspicious URL Detected\',\n        msgXSS:          \'Suspected XSS Attack\',\n        msgBlocked:      {\n            \'malicious_javascript/skimmer\': {\n                title: \'Shopping Site Skimmer Detected\',\n                description: \'shopping site skimmer\'\n            },\n            \'malicious_javascript/dropsite\': {\n                title: \'Potential Credential Leak Detected\',\n                description: \'potential credential leak\'\n            },\n            \'malicious_javascript/cryptojacker\': {\n                title: \'Web Miner Detected\',\n                description: \'web miner\'\n            },\n            \'malware_c2/infrastructure\': {\n                title: \'Malicious URL\',\n                description: \'malicious page\'\n            },\n            \'default\': {\n                title: \'Suspected Phishing\',\n                description: \'suspected phishing site\'\n            },\n        },\n\n        // Human readable reasons\n        urlTypeCrimeMappings: {\n            \'malicious_javascript/skimmer\':      evilResourceCrime,\n            \'malicious_javascript/cryptojacker\': evilResourceCrime,\n            \'malicious_javascript/dropsite\':     \' is blocked because we detected that sensitive information was leaked from\',\n        },\n\n        // Timeout function value to clear cache etc.\n        timeout:      300000,\n\n        // Skimmer whitelist update interval (1 day)\n        skimmerWhitelistInterval: 1000 * 60 * 60 * 24,\n\n        // Google Analytics account\n        account:      \'UA-2150242-8\',\n        gaCategory:   \'Popup load times\',\n\n        // InputAnalyser constants\n        minInputSize: 5,\n\n        // First addresses of the private IPv4 netblocks\n        privateIPv4Blocks: [\n            \'10.0.0.0\',\n            \'172.16.0.0\',\n            \'192.168.0.0\'\n        ],\n\n        // Sync storage key for disable for this site button whitelist\n        disableButtonWhitelistKey: \'disableButtonWhitelist\',\n\n        // Local storage key for local credential leak detection whitelist\n        localCredentialLeakWhitelistKey: \'localCredentialLeakWhitelist\',\n\n        // Whitelisted fileName regexes for credential leak detection\n        credentialLeakFileNameWhitelist: [\n            /^https?:\\/\\/googleads.g.doubleclick.net\\//,\n        ],\n    };\n})();\n\n/*\n * recursively prevents any property or sub-property in cons from:\n *   - being added;\n *   - being removed; or\n *   - being modified.\n */\nfunction deepFreeze(object) {\n    Object.freeze(object);\n\n    for (const name of Object.getOwnPropertyNames(object)) {\n        if (typeof object[name] === \'object\') {\n            deepFreeze(object[name]);\n        }\n    }\n}\n\ndeepFreeze(cons);\n/*\r\n * RelevantInput objects check whether given form inputs should be\r\n * treated as sensitive data.\r\n *\r\n * This should be treated as an abstract class. Only the subclasses\r\n * are intended to be instantiated.\r\n */\r\nclass RelevantInput {\r\n    /*\r\n     * Enum of the possible return values of test():\r\n     * RELEVANT:\r\n     *     Input is relevant now, outgoing requests should be\r\n     *     checked for the contained value (subject to minimum length).\r\n     * POTENTIALLY RELEVANT:\r\n     *     Input should not be considered relevant now, but may be in\r\n     *     future so should continue to be checked.\r\n     * NOT_RELEVANT:\r\n     *     Input is not relevant and will not become relevant, so can\r\n     *     be ommitted from further checks to optimise performance.\r\n     */\r\n    static get relevance() {\r\n        return {\r\n            NOT_RELEVANT: 1,\r\n            POTENTIALLY_RELEVANT: 2,\r\n            RELEVANT: 3,\r\n        };\r\n    }\r\n\r\n    /*\r\n     * relevantInputTypes (string) - a comma separated list of\r\n     * selectors that describe the types of input that should be\r\n     * considered relevant.\r\n     */\r\n    constructor(relevantInputTypes) {\r\n        this.relevantInputTypes = relevantInputTypes;\r\n    }\r\n\r\n    /*\r\n     * Checks whether any of the given input\'s \'labels\' (the ID, name,\r\n     * or actual label) match the given regex.\r\n     */\r\n    static testInputLabel(regex, input) {\r\n        const labels = [\r\n            input.id,\r\n            input.name,\r\n            InputAnalyser.getLabelText(input),\r\n        ];\r\n        return labels.some(label => regex.test(label));\r\n    }\r\n}\r\n\r\n/*\r\n * RelevantInput for passwords.\r\n */\r\nclass Password extends RelevantInput {\r\n    constructor() {\r\n        super(\'input[type=password]\');\r\n    }\r\n\r\n    /*\r\n     * Passwords are verified purely by their input type.\r\n     */\r\n    test(input) {\r\n        if (input.matches(this.relevantInputTypes)) {\r\n            return RelevantInput.relevance.RELEVANT;\r\n        } else {\r\n            return RelevantInput.relevance.NOT_RELEVANT;\r\n        }\r\n    }\r\n}\r\n\r\n/*\r\n * RelevantInput for card numbers.\r\n */\r\nclass CardNumber extends RelevantInput {\r\n    constructor() {\r\n        // Inputs with no type attribute act like text inputs\r\n        super(\r\n            \'input[type=text], input[type=tel], input[type=number], input:not([type])\'\r\n        );\r\n\r\n        // Definitely card number labels\r\n        this.inputLabelRegexes = [\r\n            /(cc|card).?(num|no)/i,\r\n            /カード番号/,\r\n            /Номер.*карты/i,\r\n            /信用卡卡號/i,\r\n            /信用卡号码/i,\r\n        ];\r\n\r\n        /*\r\n         * Matches incomplete card numbers, because ideally the\r\n         * skimmer should be detected before the victim gives away\r\n         * most of their card number.\r\n         */\r\n        this.inputValueRegexes = [/^(\\d{4}[-\\s]?){0,4}\\d{1,4}$/];\r\n\r\n        // Probably card number labels\r\n        this.suspectInputLabelRegexes = [\r\n            /card/i,\r\n            /tarjeta/i,\r\n            /carte/i,\r\n            /карты/i,\r\n            /卡/i,\r\n            /カード/i,\r\n            /카드/i,\r\n            /credit[^s]/i,\r\n        ];\r\n\r\n        // If it matches one of these, probably not a card number\r\n        this.inputLabelExceptionRegexes = [\r\n            /post/i,\r\n            /code/i,\r\n            /zip/i,\r\n            /acc(oun)?t?[-_]?n(um|o)/i,\r\n            /credit[-_]?(sum|limit)/i,\r\n            /loyalty/i,\r\n            /gift/i,\r\n            /idcard/i,\r\n            /cardid/i,\r\n            /club/i,\r\n            /sku/i,\r\n            /(\\b|_)otp/i,\r\n            /price/i,\r\n            /amount/i,\r\n            /service/i,\r\n            /picard/i,\r\n            /search/i,\r\n            /contact/i,\r\n            /callme/i,\r\n            /chat/i,\r\n            /activate/i,\r\n            /msisdn/i,\r\n        ];\r\n    }\r\n\r\n    test(input) {\r\n        if (!input.matches(this.relevantInputTypes)) {\r\n            return RelevantInput.relevance.NOT_RELEVANT;\r\n        }\r\n\r\n        // Return NOT_RELEVANT if label matches an exception regex\r\n        if (\r\n            this.inputLabelExceptionRegexes.some(r =>\r\n                RelevantInput.testInputLabel(r, input)\r\n            )\r\n        ) {\r\n            return RelevantInput.relevance.NOT_RELEVANT;\r\n        }\r\n\r\n        // Return RELEVANT if label clearly states card number\r\n        if (\r\n            this.inputLabelRegexes.some(r =>\r\n                RelevantInput.testInputLabel(r, input)\r\n            )\r\n        ) {\r\n            return RelevantInput.relevance.RELEVANT;\r\n        }\r\n\r\n        /*\r\n         * If label suggests card number, return RELEVANT if value\r\n         * also suggests card number or POTENTIALLY_RELEVANT if\r\n         * not. Otherwise, return NOT_RELEVANT as this element will\r\n         * not become relevant unless a label changes (unlikely).\r\n         */\r\n        if (\r\n            this.suspectInputLabelRegexes.some(r =>\r\n                RelevantInput.testInputLabel(r, input)\r\n            )\r\n        ) {\r\n            if (this.inputValueRegexes.some(r => r.test(input.value))) {\r\n                return RelevantInput.relevance.RELEVANT;\r\n            } else {\r\n                return RelevantInput.relevance.POTENTIALLY_RELEVANT;\r\n            }\r\n        } else {\r\n            return RelevantInput.relevance.NOT_RELEVANT;\r\n        }\r\n    }\r\n}\r\n\n    const inputAnalyser = new InputAnalyser();\n\n    const resourceBlockList = new Set([\n                            \"6C590F97DDB1A11AE795B3050E4C3866\", \"D285BE79786E2ECE396941331C5BA65E\", \"59DF8523520DE7F08FB53D3D12BD94F5\", \"1FB784EBAFDA1618023756BF5B6D7E46\", \"0D6E1B5F03B81F5C37813B22E29C4FE2\", \"EFAB0ECE9383D50C555B8168C3E2A4DD\", \"AC8A4B13A411A4E43E61D0A034FA4A55\", \"25A781F6D1F94EEACCF1E00E844D9E14\", \"3920B04ED0D215EA738F524F60485F23\", \"05F4A2D3161A92EB82A44F15FC2A3AEC\", \"C5A1BE7B3855B0A25275B2877E005CB9\", \"00069CEC347076721EBE2257978A0A33\", \"9A513130D02F360BBDC16D8AC0C579D8\", \"7CDB2A3A29E50F90C1000252EF523AD6\", \"25CE912FAF2376026AAD0174AD5A0329\", \"739E1F67AA5F64A06FC469F6030802D4\", \"F49368C9A05C29EF49411999C5987F2C\", \"AB2DD89FB892155275D8780CF527BC93\", \"CFE758EB18FBFE70B1F690241B75D2B1\", \"E43C3463B40A790545F4149FF06B419E\", \"8B6D4C76A7CCF5EB720660B8B2D3F5EB\", \"45573B3E2F613AF5BF2145DF004BABD1\", \"E3DB4E968B371DE94CE607199A062F39\", \"A4EFF1240C5DE614B394FB738CBD28E2\", \"F42D10E84B9A69A8A24062319F425A83\", \"90646BE39982594A58EDF5D2630CCFFD\", \"669853BEA9B84EA51D7302A7FBFF5BC3\", \"0BA566B4777526F451108100107DC5ED\", \"AB19E12BFFBC47244BDCC2403EAA9037\", \"61DAF8537240639277FD1A5908A131A4\", \"6F1DC1309CFE734C038685D8F17A3B6B\", \"DCC3DB001334F29C1C0208D15E9EAB49\", \"2DBD7D54022C28273ADE3147B8EEB076\", \"AE3760E93412C65EC5BF63537B037019\", \"4CD96A830DAC0E0B5EBE578B0DD870AE\", \"4C70569BBE1821F5655382660807FA77\", \"05ECA17FF0EA78669ED43B2BD7B63571\", \"44E503B3621513B9D3666F284E4DF47A\", \"8F1C92E7665860AE70CBB228DED86111\", \"D69552885B55C4097E5725F805815639\", \"B4357A9D889E974CDF3F9740E48F9A1B\", \"5709DC60CFC365B7064B06C709A9D32B\", \"14E8729CBB16AC3625EAA0D7A679E56E\", \"793D4DABAAA377A12F2B6CE88C5FB389\", \"C5D1DCE2AE0D8CE79054FC16CB13D65A\", \"89F91750BD4A9397D1F8CE183757FF70\", \"50B8BC213FB188A687173C1A0A77EA57\"\n                        ]);\n    const evilJSPatterns = new Set([\n                            \n                        ]);\n\n    // Extract body object\n    function extractBody(data) {\n        if (!data) {\n            return Promise.resolve({});\n        }\n        if (FormData.prototype.isPrototypeOf(data)) {\n            const formData = {};\n            for (const [key, value] of data.entries()) {\n                formData[key] = value;\n            }\n            return Promise.resolve({ formData });\n        }\n        // Create a temporary Request object to parse the body\n        const tempRequest = new Request(\'\', { body: data, method: \'POST\' });\n        return extractBodyFromRequest(tempRequest);\n    }\n\n    function extractBodyFromRequest(request) {\n        // Try to extract as text\n        return request\n            .text()\n            .then(text => {\n                return { text };\n            })\n            .catch(e => {\n                return {};\n            });\n    }\n\n    /**\n     * This function checks if the request hostname/URL is in the blocklist.\n     * Used when identifying whether or not to forward request to background\n     * script.\n     * @param hostname Hostname to check against blocklist.\n     * @param url Request url to check against evil JS patterns\n     * @returns boolean true if hostname is in the blocklist, false if\n     * it is not in blocklist or if blocklist is not provided.\n     */\n    function requestInBlocklist(hostname, url) {\n        if (resourceBlockList === undefined || evilJSPatterns === undefined) {\n            return false;\n        }\n        if (testEvilJS(url.replace(/^(https?:)?\\/\\//i, \'\'))) {\n            return true;\n        }\n        while (!testEvilJS(hostname) && !testResource(hostname)) {\n            const newHostname = hostname.replace(/^.+?\\./g, \'\');\n\n            if (hostname === newHostname) return false;\n\n            hostname = newHostname;\n        }\n        return true;\n    }\n\n    function testEvilJS(url) {\n        return evilJSPatterns.has(base64hash(sha512, url));\n    }\n\n    function testResource(hostname) {\n        return resourceBlockList.has(\n            md5(cons.evilResourcesSalt + hostname).toUpperCase()\n        );\n    }\n\n    /*\n     * Implementations to overwrite window.XMLHttpRequest.prototype.open,\n     * window.XMLHttpRequest.prototype.send and window.fetch. These are (to my knowledge) the two\n     * APIs that can be used to make requests within Javascript. If there are more, then they will\n     * also need to be overridden and intercepted like below.\n     */\n\n    /**\n     * This function generates functions to be used for wrapping and intercepting network\n     * requests, namely from XMLHttpRequest and the Fetch API. It works by taking the real\n     * function we want to intercept the request for, and then uses document.currentScript to work\n     * out which file initiated the request. It sends those details to the content script which\n     * forwards it to the background script, and then we await a confirmation of receipt from the\n     * background page. Finally, the real request is sent by forwarding parameters to the original\n     * function passed in.\n     * @param realFn A reference to the real function that is being intercepted.\n     * @param getRequestUrlFn A function used to get the request URL from the object. This is\n     * necessary as the mechanisms for different objects differs depending on implementation.\n     * @param getRequestBodyPr A function used to get the request body from the object. Returns\n     * a promise. It should be null if it\'s not applicable (e.g. if it sets the src of an image or script).\n     * @param type The type of request, which will be a ResourceType:\n     * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType\n     * @returns {function(...[*]=): PromiseLike<any | never>}\n     */\n    function generateRequestInterceptFn(\n        realFn,\n        getRequestUrlFn,\n        getRequestBodyPr,\n        type\n    ) {\n        const patched = function(...args) {\n            /*\n             * We create a link DOM element in order to correctly generate the\n             * absolute path of the request, automatically transforming from a\n             * relative path if necessary.\n             */\n            const requestUrl = normaliseUrl(\n                getRequestUrlFn.bind(this)(...args)\n            );\n\n            let requestHostname;\n            let pageHostname;\n\n            try {\n                requestHostname = new URL(requestUrl).hostname;\n                pageHostname = new URL(location.href).hostname;\n            } catch (e) {\n                return realFn.apply(this, args);\n            }\n\n            /*\n             * If hostname is in the blocklist, the request should always be\n             * sent to the background script.\n             */\n            // eslint-disable-next-line prefer-const\n            let forceStall = false;\n\n            if (!forceStall) {\n                /*\n                 * Delaying outgoing requests sometimes causes problems\n                 * with elements such as video players and dynamically\n                 * loaded images, so it should only be done when\n                 * necessary. Before inputAnalyser is defined and when the\n                 * request is to the same hostname as the page, this can\n                 * be skipped.\n                 */\n                if (\n                    requestHostname === pageHostname ||\n                    inputAnalyser === undefined\n                ) {\n                    return realFn.apply(this, args);\n                }\n\n                /*\n                 * It can also be skipped if there is no sensitive\n                 * information entered into a form on the page yet.\n                 */\n                const relevantFilledInputs = inputAnalyser.relevantFilledInputs;\n                if (!relevantFilledInputs.length) {\n                    return realFn.apply(this, args);\n                }\n\n                /*\n                 * For requests with no body, it\'s only necessary to delay\n                 * them when the sensitive information from the form\n                 * inputs is present in the request URL.\n                 */\n                const ia = new InputAnalyser(relevantFilledInputs);\n                const relevantInputValues = ia.relevantInputValues;\n                const urlMatcher = new URLParamsMatcher(requestUrl);\n\n                if (\n                    getRequestBodyPr === null &&\n                    !urlMatcher.findInputMatches(relevantInputValues).length\n                ) {\n                    return realFn.apply(this, args);\n                }\n            }\n\n            /*\n             * Create a promise that resolves immediately if the request body\n             * does not apply to this request.\n             */\n            if (getRequestBodyPr === null) {\n                getRequestBodyPr = function() {\n                    return Promise.resolve({});\n                };\n            }\n\n            let fileName;\n            if (document.currentScript && document.currentScript.src) {\n                fileName = document.currentScript.src;\n            } else if (callbackCurrentScript && callbackCurrentScript.src) {\n                fileName = callbackCurrentScript.src;\n            } else {\n                fileName = location.href;\n            }\n\n            return getRequestBodyPr\n                .bind(this)(...args)\n                .then(requestBody => {\n                    /*\n                     * Generate the information object to send through to the\n                     * content script including the request URL, the initiator\n                     * filename and a timestamp relative to the page load.\n                     */\n                    const detail = {\n                        fileName,\n                        requestBody,\n                        requestUrl,\n                        timestamp: performance.now(),\n                        type,\n                    };\n\n                    document.dispatchEvent(\n                        new CustomEvent(cons.capturedIntercept, {\n                            detail,\n                        })\n                    );\n\n                    return new Promise(resolve => {\n                        /*\n                         * Send the initiator\'s file name and other info to the\n                         * background script, then wait for confirmation that the\n                         * details made it to the background page before calling\n                         * the real function. This is to avoid any race conditions\n                         * whereby the details haven\'t been added to the tab data\n                         * before onBeforeRequest/onBeforeSendHeaders gets called.\n                         */\n                        document.addEventListener(\n                            cons.confirmedIntercept,\n                            function compareData(customEvent) {\n                                const receivedData = JSON.parse(\n                                    customEvent.detail\n                                );\n                                if (detailsMatch(detail, receivedData)) {\n                                    /*\n                                     * If it\'s the correct confirmation, then we\n                                     * can resolve the promise and call the real\n                                     * function in the finally block.\n                                     */\n                                    document.removeEventListener(\n                                        cons.confirmedIntercept,\n                                        compareData,\n                                        false\n                                    );\n                                    resolve();\n                                }\n                            }\n                        );\n                    }).then(() => realFn.apply(this, args));\n                });\n        };\n        // make toString() look native, #34\n        patched.toString = () => realFn.toString();\n        return patched;\n    }\n\n    function detailsMatch(first, second) {\n        const comparisonKeys = [\'fileName\', \'requestUrl\', \'timestamp\', \'type\'];\n        for (const key of comparisonKeys) {\n            if (first[key] !== second[key]) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    window.fetch = generateRequestInterceptFn(\n        realFetch,\n        request => (typeof request === \'string\' ? request : request.url),\n        (request, options) => {\n            // Check if argument is a Request object.\n            if (request instanceof Request) {\n                return extractBodyFromRequest(request);\n            }\n            return options ? extractBody(options.body) : Promise.resolve({});\n        },\n        \'xmlhttprequest\'\n    );\n\n    window.XMLHttpRequest.prototype.send = generateRequestInterceptFn(\n        realSend,\n        function getRequestUrl() {\n            return this._requestUrl;\n        },\n        extractBody,\n        \'xmlhttprequest\'\n    );\n\n    /*\n     * This function is intercepted so that we can capture the value of the\n     * request URL, since it isn\'t present in the arguments for\n     * XMLHttpRequest.send (above).\n     */\n    window.XMLHttpRequest.prototype.open = function open(...args) {\n        [, this._requestUrl] = args;\n        realOpen.apply(this, args);\n    };\n    window.XMLHttpRequest.prototype.open.toString = () => realOpen.toString();\n\n    /*\n     * Monkeypatches for setting the src of images and scripts, an\n     * indirect way of sending requests that is often employed by\n     * skimmers. The getters are also overridden to make the behaviour\n     * seem identical to the original implementation. Otherwise, if a\n     * script were to set an src and immediately read it, it would\n     * come back as the old value.\n     */\n\n    /*\n     * WeakMap maps an object to a value. This is used to store the\n     * new src of an object when its true src property may not yet\n     * have been set.\n     */\n    const srcMap = new WeakMap();\n    const requestElements = [\n        {\n            srcElement: HTMLImageElement.prototype,\n            type: \'image\',\n        },\n        {\n            srcElement: HTMLScriptElement.prototype,\n            type: \'script\',\n        },\n    ];\n\n    for (const { srcElement, type } of requestElements) {\n        const realGetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\')\n            .get;\n        const realSetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\')\n            .set;\n        const realGetAttribute = srcElement.getAttribute;\n        const realSetAttribute = srcElement.setAttribute;\n        const realRemoveAttribute = srcElement.removeAttribute;\n\n        const interceptFn = generateRequestInterceptFn(\n            realSetSrc,\n            src => `${src}`,\n            null,\n            type\n        );\n\n        function setSrc(url) {\n            /*\n             * It\'s valid to provide a TrustedScriptURL wrapping the URL but we need to\n             * return a string in the getter, so we convert the url to a string here.\n             *\n             * The url can also be undefined/null so we can\'t just call `.toString()`.\n             *\n             * See #56.\n             */\n            srcMap.set(this, `${url}`);\n            interceptFn.call(this, url);\n            return url;\n        }\n\n        function getSrc() {\n            if (srcMap.get(this) === undefined) {\n                return realGetSrc.call(this);\n            } else {\n                return normaliseUrl(srcMap.get(this));\n            }\n        }\n\n        getSrc.toString = () => realGetSrc.toString();\n        setSrc.toString = () => realSetSrc.toString();\n\n        Object.defineProperty(srcElement, \'src\', {\n            get: getSrc,\n            set: setSrc,\n        });\n\n        srcElement.setAttribute = function(attributeName, value) {\n            if (attributeName.toUpperCase() === \'SRC\') {\n                setSrc.call(this, value);\n            } else {\n                realSetAttribute.call(this, attributeName, value);\n            }\n        };\n        srcElement.setAttribute.toString = () => realSetAttribute.toString();\n\n        srcElement.getAttribute = function(attributeName) {\n            if (\n                attributeName.toUpperCase() === \'SRC\' &&\n                srcMap.get(this) !== undefined\n            ) {\n                return srcMap.get(this);\n            } else {\n                return realGetAttribute.call(this, attributeName);\n            }\n        };\n        srcElement.getAttribute.toString = () => realGetAttribute.toString();\n\n        srcElement.removeAttribute = function(attributeName) {\n            if (attributeName.toUpperCase() === \'SRC\') {\n                srcMap.delete(this);\n            }\n\n            return realRemoveAttribute.call(this, attributeName);\n        };\n        srcElement.removeAttribute.toString = () =>\n            realRemoveAttribute.toString();\n    }\n})(\n    window.XMLHttpRequest.prototype.open,\n    window.XMLHttpRequest.prototype.send,\n    window.fetch\n);\n</script><script>/*\n * This entire block is wrapped in an IIFE to prevent polluting the scope of the web page with\n * functions created by this extension.\n */\n(function(realOpen, realSend, realFetch) {\n    /*\n     * Sometimes pages overwrite standard objects with global\n     * variables. Where possible, we extract fresh versions from an\n     * iframe.\n     */\n    const iframe = document.createElement(\'iframe\');\n    document.documentElement.appendChild(iframe);\n\n    // Initialise with global values\n    let URL = window.URL;\n    let WeakMap = window.WeakMap;\n    let WeakSet = window.WeakSet;\n    let TextDecoder = window.TextDecoder;\n    let Uint8Array = window.Uint8Array;\n    let decodeURIComponent = window.decodeURIComponent;\n    let URLSearchParams = window.URLSearchParams;\n    let Array = window.Array;\n    let Object = window.Object;\n    let atob = window.atob;\n    let CustomEvent = window.CustomEvent;\n    let performance = window.performance;\n    let JSON = window.JSON;\n\n    /*\n     * Under some circumstances the iframe isn\'t added\n     * successfully. If it has a contentWindow then we can extract the\n     * values from it, but otherwise we fall back on the global\n     * values.\n     */\n    if (iframe.contentWindow) {\n        // Try block because sometimes the CORS prevents access to iframe\n        try {\n            URL = iframe.contentWindow.URL;\n            WeakMap = iframe.contentWindow.WeakMap;\n            WeakSet = iframe.contentWindow.WeakSet;\n            TextDecoder = iframe.contentWindow.TextDecoder;\n            Uint8Array = iframe.contentWindow.Uint8Array;\n            decodeURIComponent = iframe.contentWindow.decodeURIComponent;\n            URLSearchParams = iframe.contentWindow.URLSearchParams;\n            Array = iframe.contentWindow.Array;\n            Object = iframe.contentWindow.Object;\n            atob = iframe.contentWindow.atob;\n            CustomEvent = iframe.contentWindow.CustomEvent;\n            performance = iframe.contentWindow.performance;\n            JSON = iframe.contentWindow.JSON;\n        } catch (error) {\n            // Empty catch block to prevent errors from showing in page logs\n        }\n    }\n\n    // If iframe was added successfully, remove it\n    if (iframe.parentNode) {\n        iframe.parentNode.removeChild(iframe);\n    }\n\n    /*\n     * If any of the values don\'t have the correct type, return and\n     * don\'t attempt any monkey patching. This avoids breaking web\n     * pages.\n     */\n    for (f of [\n        URL,\n        WeakMap,\n        WeakSet,\n        TextDecoder,\n        Uint8Array,\n        decodeURIComponent,\n        URLSearchParams,\n        Array,\n        Object,\n        atob,\n        CustomEvent,\n    ]) {\n        if (typeof f !== \'function\') {\n            return;\n        }\n    }\n\n    for (o of [performance, JSON]) {\n        if (typeof o !== \'object\') {\n            return;\n        }\n    }\n\n    /*\n     * Override common funcions that accept callbacks, such as\n     * setInterval, to keep track of what script each callback comes\n     * from. Skimmers commonly have the actual skimming code in a\n     * callback, so most of the time when we intercept a request,\n     * document.currentScript is null. To solve this, the script is\n     * stored in a closure when the callback is passed, and assigned\n     * to callbackCurrentscript when the callback is run.\n     */\n\n    let callbackCurrentScript = null;\n\n    // Maps callbacks originally passed to patched versions\n    const patchedCallbackMap = new WeakMap();\n\n    // Maps patched callbacks to original versions\n    const originalCallbackMap = new WeakMap();\n\n    /**\n     *  Generates the function with which to override the\n     *  callback-accepting functions.\n     *  @param realFn The real function, e.g. setInterval.\n     *  @param callbackIndex The index of the callback in the\n     *  arguments of realFn. For example, the callback is the first\n     *  argument in setInterval, so callbackIndex should be 0.\n     */\n    function generateAddCallbackFunction(realFn, callbackIndex) {\n        const patched = function(...args) {\n            const script = document.currentScript || callbackCurrentScript;\n\n            const callback = args[callbackIndex];\n\n            // If the callback isn\'t a function, there\'s nothing to patch\n            if (typeof callback !== \'function\') {\n                return realFn.apply(this, args);\n            }\n\n            // If a patch exists, use it instead of creating new function\n            let patchedCallback = patchedCallbackMap.get(callback);\n            if (!patchedCallback) {\n                patchedCallback = function() {\n                    callbackCurrentScript = script;\n                    const retVal = callback.apply(this, [...arguments]);\n                    callbackCurrentScript = null;\n                    return retVal;\n                };\n                patchedCallbackMap.set(callback, patchedCallback);\n                originalCallbackMap.set(patchedCallback, callback);\n            }\n\n            args[callbackIndex] = patchedCallback;\n            return realFn.apply(this, args);\n        };\n        // make toString() look native, #34\n        patched.toString = () => realFn.toString();\n        return patched;\n    }\n\n    const realSetInterval = window.setInterval;\n\n    window.setInterval = generateAddCallbackFunction(realSetInterval, 0);\n\n    const realSetTimeout = window.setTimeout;\n\n    window.setTimeout = generateAddCallbackFunction(realSetTimeout, 0);\n\n    const realPromiseThen = Promise.prototype.then;\n    Promise.prototype.then = generateAddCallbackFunction(realPromiseThen, 0);\n\n    const realPromiseCatch = Promise.prototype.catch;\n    Promise.prototype.catch = generateAddCallbackFunction(realPromiseCatch, 0);\n\n    const realPromiseFinally = Promise.prototype.finally;\n    if (realPromiseFinally) {\n        Promise.prototype.finally = generateAddCallbackFunction(\n            realPromiseFinally,\n            0\n        );\n    }\n\n    const realAddEventListener = EventTarget.prototype.addEventListener;\n\n    EventTarget.prototype.addEventListener = generateAddCallbackFunction(\n        realAddEventListener,\n        1\n    );\n\n    /*\n     * removeEventListener needs to be patched to map the passed\n     * listener to the patched listener.\n     */\n    const realRemoveEventListener = EventTarget.prototype.removeEventListener;\n    EventTarget.prototype.removeEventListener = function(...args) {\n        args[1] = patchedCallbackMap.get(args[1]) || args[1];\n\n        return realRemoveEventListener.apply(this, args);\n    };\n    EventTarget.prototype.removeEventListener.toString = () =>\n        realRemoveEventListener.toString();\n\n    /*\n     * Event listeners are sometimes set by directly assigning to a\n     * property from the GlobalEventHandlers mixin,\n     * e.g. \'submitBtn.onclick\'. For each of the GlobalEventHandlers\n     * implementations that are likely to be relevant, the event\n     * properties likely to be used by skimmers are overridden with\n     * our version that tracks currentScript when assigned.\n     */\n    for (const eventTarget of [\n        HTMLElement.prototype,\n        Document.prototype,\n        window,\n    ]) {\n        for (const eventName of [\n            \'onclick\',\n            \'onblur\',\n            \'onfocus\',\n            \'onchange\',\n            \'oninput\',\n            \'onkeydown\',\n            \'onkeypress\',\n            \'onkeyup\',\n            \'onload\',\n            \'onloadend\',\n            \'onmousedown\',\n            \'onmouseenter\',\n            \'onmouseleave\',\n            \'onmousemove\',\n            \'onmouseout\',\n            \'onmouseover\',\n            \'onmouseup\',\n            \'onpointerdown\',\n            \'onpointermove\',\n            \'onpointerup\',\n            \'onpointerover\',\n            \'onpointerout\',\n            \'onpointerenter\',\n            \'onpointerleave\',\n            \'onsubmit\',\n        ]) {\n            const descriptor = Object.getOwnPropertyDescriptor(\n                eventTarget,\n                eventName\n            );\n\n            if (!descriptor) {\n                continue;\n            }\n\n            const realGetter = descriptor.get;\n            const realSetter = descriptor.set;\n\n            const patchedSetter = generateAddCallbackFunction(realSetter, 0);\n\n            /*\n             * In case a script checks the value of the property after\n             * setting it, the getter has to be patched to return the\n             * original callback.\n             */\n            const patchedGetter = function() {\n                const realValue = realGetter.call(this);\n\n                const originalCallback = originalCallbackMap.get(realValue);\n                if (originalCallback) {\n                    return originalCallback;\n                } else {\n                    return realValue;\n                }\n            };\n\n            patchedGetter.toString = () => realGetter.toString();\n\n            Object.defineProperty(eventTarget, eventName, {\n                get: patchedGetter,\n                set: patchedSetter,\n            });\n        }\n    }\n\n    /*\n     * This script has some dependencies that need to be included\n     * without polluting the global namespace. This is achieved by\n     * inserting them into the source at build time in the\n     * makefile. This is preferable to loading the dependencies via\n     * AJAX, because the latter doesn\'t work if the CSP disallows\n     * unsafe-eval. The following tag tells the makefile where to\n     * insert the dependencies.\n     */\n    // <DEPENDENCIES>\nclass InputAnalyser {\n    constructor(_inputs) {\n        this._inputs = _inputs;\n        this._irrelevantInputs = new WeakSet();\n\n        /*\n         * Not currently using Password because it was causing too\n         * many false positives. Some sites use external auth domains,\n         * and many have misconfigured analytics that collect\n         * passwords.\n         */\n        this.relevantInputs = [new CardNumber()];\n    }\n\n    get relevantInputValues() {\n        const inputs = {};\n        this._inputs.forEach(\n            input => (inputs[input.id || input.name] = input.value)\n        );\n\n        return inputs;\n    }\n\n    get relevantFilledInputs() {\n        return this.allInputs\n            .filter(input => this.isRelevantElement(input))\n            .filter(input => input.value.length >= cons.minInputSize)\n            .map(input => ({\n                id: input.id,\n                name: input.name,\n                value: input.value,\n            }));\n    }\n\n    get allInputs() {\n        this._inputs = [\n            ...document.querySelectorAll(\n                this.relevantInputs.map(i => i.relevantInputTypes).join(\', \')\n            ),\n        ];\n\n        return this._inputs;\n    }\n\n    static getLabelText(input) {\n        if (!input.labels.length) {\n            return \'\';\n        }\n        return stripSpecialChars(input.labels[0].textContent).trim();\n    }\n\n    /**\n     * Takes an HTMLElement and determines whether or not it could be relevant for the purposes of\n     * skimmer detection.\n     * @param {HTMLElement} input The element which is to be checked for relevancy.\n     * @returns {boolean} Returns true if the element is potentially relevant (e.g. a credit card\n     * field or other such sensitive input).\n     */\n    isRelevantElement(input) {\n        // If the element has been identified as irrelevant, return false\n        if (this._irrelevantInputs.has(input)) {\n            return false;\n        }\n\n        // Check the input against all the relevant input types\n        const relevances = this.relevantInputs.map(i => i.test(input));\n\n        /*\n         *  If they all agree that this input is unlikely to become\n         *  relevant, exclude it from further checks and return false\n         */\n        if (relevances.every(i => i === RelevantInput.relevance.NOT_RELEVANT)) {\n            this._irrelevantInputs.add(input);\n            return false;\n        }\n\n        // Return true if any consider the input to be relevant, otherwise false\n        return relevances.some(i => i === RelevantInput.relevance.RELEVANT);\n    }\n}\n/*\n * Input Matcher represents a class that can have matches with relevant inputs.\n * Each class that inherits must implement the findInputMatches method.\n */\nclass InputMatcher {\n    /*\n     * Finds any matches with the list of inputs.\n     * @param inputs. An object whose values represent relevant inputs.\n     * @return {[String]} The keys of the inputs that were found.\n     */\n    findInputMatches(inputs) {\n        throw new Error(\'findInputMatches has not been implemented.\');\n    }\n\n    /**\n     * Determines whether one or more values in needles are present in any of\n     * the keys of the haystacks.\n     * Checks both raw input and base64 encoded versions of the needles.\n     * @param needles An object. The haystacks are searched for the values of\n     * this object.\n     * @param haystacks The array of strings in which the needle(s) may reside.\n     * A string may optionally be supplied if only one value is present.\n     * @returns {[String]} The keys of the needles that were found in one or\n     * more haystacks in either raw or base64 encoded form.\n     */\n    static atLeastOneNeedleInHaystack(needles, haystacks) {\n        if (\n            needles === undefined ||\n            needles.length === 0 ||\n            haystacks === undefined\n        ) {\n            return [];\n        }\n\n        const searchArray = Array.isArray(haystacks) ? haystacks : [haystacks];\n\n        return Object.entries(needles)\n            .filter(([_, needle]) =>\n                searchArray.some(haystack => {\n                    if (isBase64Encoded(haystack)) {\n                        if (atob(haystack).indexOf(needle) !== -1) {\n                            return true;\n                        }\n                    }\n                    return haystack.indexOf(needle) !== -1;\n                })\n            )\n            .map(([input, _]) => input);\n    }\n}\n/*\n * Matcher for an object that cannot contain any inputs.\n */\nclass EmptyMatcher extends InputMatcher {\n    findInputMatches(inputs) {\n        return [];\n    }\n}\n\nclass URLParamsMatcher extends InputMatcher {\n    constructor(url) {\n        super();\n        this._url = url;\n    }\n\n    findInputMatches(inputs) {\n        try {\n            const parsedUrl = new URL(this._url);\n            return InputMatcher.atLeastOneNeedleInHaystack(inputs, [\n                ...parsedUrl.searchParams.values(),\n            ]);\n        } catch (e) {\n            return [];\n        }\n    }\n}\n\nclass StringBodyMatcher extends InputMatcher {\n    constructor(str) {\n        super();\n        this._str = str;\n    }\n\n    findInputMatches(inputs) {\n        let decoded;\n        try {\n            decoded = decodeURIComponent(this._str);\n        } catch (e) {\n            decoded = this._str;\n        }\n\n        const searchParams = new URLSearchParams(decoded);\n        const parsedQuery = {};\n        const parsedValues = [];\n\n        for (const [key, value] of searchParams.entries()) {\n            parsedQuery[key] = value;\n            parsedValues.push(value);\n        }\n\n        if (parsedQuery !== {} && parsedValues.indexOf(null) === -1) {\n            return InputMatcher.atLeastOneNeedleInHaystack(\n                inputs,\n                parsedValues\n            );\n        }\n        return InputMatcher.atLeastOneNeedleInHaystack(inputs, decoded);\n    }\n}\n\nclass FormBodyMatcher extends InputMatcher {\n    constructor(formData) {\n        super();\n        this._formData = formData;\n    }\n\n    findInputMatches(inputs) {\n        return InputMatcher.atLeastOneNeedleInHaystack(\n            inputs,\n            Object.values(this._formData).flat()\n        );\n    }\n}\n/**\n * Takes a string and removes a variety of \'special\' characters like punctuation.\n * @param str The string to be modified.\n * @returns {string} The resulting string without any special characters.\n */\nfunction stripSpecialChars(str) {\n    return str.replace(/[`~!@#$%^&*()_|+\\-=?;:\'\",.<>}{\\][\\\\/]/gi, \'\');\n}\n\n/**\n * Attempts to identify where a string is base-64 encoded or not.\n * @param string The raw string to be examined.\n * @returns {boolean} Returns true if the string matches a base-64 string. False otherwise.\n */\nfunction isBase64Encoded(string) {\n    return /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/.test(\n        string\n    );\n}\n\n// Computes the base64 sha512/sha224 hash of some text, removing trailing ==\nfunction base64hash(hashFunction, text) {\n    return btoa(\n        hashFunction\n            .array(text)\n            .map(char => String.fromCharCode(char))\n            .join(\'\')\n    ).slice(0, -2);\n}\n\nfunction normaliseUrl(str) {\n    const link = document.createElement(\'a\');\n    link.href = str;\n    return link.href;\n}\n\nfunction stripCredentials(urlstr) {\n    try {\n        const url = new URL(urlstr);\n        const params = url.searchParams;\n        params.forEach((_, key) => params.set(key, \'\'));\n        return url.toString();\n    } catch (e) {\n        return urlstr;\n    }\n}\n\n/**\n * Check if two URLs match regardless of any partial encoding.\n *\n * Uses decodeURI which can throw a URIError when url is malformed.\n */\nfunction urlsMatch(first, second) {\n    try {\n        return decodeURI(first) === decodeURI(second);\n    } catch (e) {\n        return false;\n    }\n}\n\nfunction sendMessage(type, data, tabId, options) {\n    if (tabId) {\n        chrome.tabs.sendMessage(tabId, { data, type }, options);\n    } else {\n        chrome.runtime.sendMessage({ data, type });\n    }\n}\n\n// Encodes the given input using base64url as defined by RFC4648 section 5.\nfunction base64Url(input) {\n    return btoa(input)\n        .replace(\'+\', \'-\')\n        .replace(\'/\', \'_\');\n}\n/*\n * Netcraft Extension\n * constants JavaScript\n */\n\n/* eslint sort-keys: \'off\' */\n/* sort-keys is disabled as the constants are separated by type */\n\n// Aligned values here make for easier reading, so we disable prettier\n\n// prettier-ignore\nconst cons = (function() {\n    const cryptojackersUrl = \'https://www.netcraft.com/apps/help/glossary/#crypto-miners\';\n    const skimmersUrl = \'https://www.netcraft.com/apps/help/glossary/#credit-card-skimmers\';\n    const evilResourceCrime = \'attempted to load\';\n\n    return {\n        fields: [\n            \'topsites\',\n            \'country\',\n            \'netblock\',\n            \'hoster\',\n            \'firstseen\',\n            \'rank\',\n            \'risk\',\n            \'uses_sslv3\',\n            \'pfs\',\n            \'heartbleed\',\n            \'heartbleed_message\',\n        ],\n\n        // options that can be toggle in options.js and installed flag\n\n        toggleOptions: [\n            \'installed\',\n            \'block\',\n            \'xss\',\n            \'suspicious\',\n            \'other-malicious-scripts\',\n            \'credential-leaks\',\n            \'analytics\',\n            \'cryptojackers\',\n            \'skimmers\',\n        ],\n\n        // domains\n        mirror:  \'https://mirror2.extension.netcraft.com/\',\n        toolbar: \'https://mirror.toolbar.netcraft.com/\',\n\n        // links and faqs\n        blogArchive:      \'https://news.netcraft.com/archives/\',\n        browserApp:       \'https://www.netcraft.com/apps/browser/\',\n        cryptojackersUrl,\n        extensionStore:   \'https://addons.mozilla.org/en-us/firefox/addon/netcraft-toolbar?src=external-blocked_visit-rating\',\n        heartbleedUrl:    \'https://www.netcraft.com/apps/help/glossary/#heartbleed\',\n        logBlockedVisit:  \'https://toolbar.netcraft.com/blocked_visit\',\n        netblock:         \'https://sitereport.netcraft.com/netblock?q=\',\n        pfsUrl:           \'https://www.netcraft.com/apps/help/glossary/#pfs\',\n        report:           \'https://sitereport.netcraft.com/?url=\',\n        reportAPI:        \'https://report.netcraft.com/api/v2/report/urls\',\n        reportMistake:    \'https://report.netcraft.com/report/mistake?url=\',\n        skimmersUrl,\n        sslv3Url:         \'https://www.netcraft.com/apps/help/glossary/#sslv3\',\n        submissionURL:    \'https://report.netcraft.com/submission/\',\n        topsites:         \'https://trends.netcraft.com/topsites?s=\',\n\n        // paths\n        block:              \'blocked.html\',\n        check:              \'check_url/v4/\',\n        danger:             \'Images/danger.png\',\n        embed:              \'/embedded\',\n        evilJSPatternsFeed: \'eviljspatterns\',\n        skimmerDomainWhitelistFeed: \'eviljsskimmerwhitelist\',\n        evilResourcesFeed:  \'blockdb/netcraft_extension_evil_resource_encrypted-yppL7bVh/\',\n        flags:              \'Images/flags/\',\n        force:              \'/dodns\',\n        heartbleed:         \'Images/heartbleed.png\',\n        info:               \'/info\',\n        poodlePost:         \'2014/10/15/googles-poodle-affects-oodles.html\',\n        warning:            \'Images/warning.png\',\n\n        // feed constants\n        evilResourceTypes: [\'cryptojacker\', \'skimmer\'],\n        evilResourcesSalt: \'C4jhYHxW\',\n        maxRevisions:      5,\n        randomSaltLength:  8,\n\n        // mappings of url_type => information\n        urlTypeInformationMappings: {\n            \'malicious_javascript/cryptojacker\': [\'web miners\',             cryptojackersUrl],\n            \'malicious_javascript/skimmer\':      [\'shopping site skimmers\', skimmersUrl],\n        },\n\n        // mappings of evilResourceTypes => user options\n        evilResourceOptionMappings: {\n            \'cryptojacker\': \'cryptojackers\',\n            \'skimmer\':      \'skimmers\'\n        },\n\n        // mappings of user options => url_type\n        optionUrlTypeMappings: {\n            \'block\':                   /^(?!malicious_javascript:)/,\n            \'cryptojackers\':           /^malicious_javascript\\/cryptojacker$/,\n            \'other-malicious-scripts\': /^malicious_javascript\\/(?!(skimmer|cryptojacker)$)/,\n            \'skimmers\':                /^malicious_javascript\\/skimmer$/,\n        },\n\n        // report constants\n        malwareSourceEmail:  \'extension-reported-malware@netcraft.com\',\n        malwareSourceUUID:   \'mwcCcApi89cobY9loJ9rWJ9HuOhlpwMj\',\n        extensionSourceUUID: \'qBEQ3NhEnwK4sypmuSxBJzRqpxZLRXxe\',\n\n        // report error default message\n        reportErrorMessage: \'An error occured with your submission. Please try again.\',\n\n        // mapping from report API errors to displayed messages\n        reportErrorMappings: {\n            \'Duplicate of a recent submission\': \'This URL has already been reported.\',\n            \'Supplied email is not valid\': \'Invalid email address. Please correct it and try again.\',\n        },\n\n        whitelistErrorMessages: {\n            invalidHostname: \'Invalid hostname, please correct it and try again\',\n            duplicate: \'Protection is already disabled for this site\',\n        },\n\n        // browserAction icon paths\n        iconNormal: {\n            16: \'Images/action-normal-16.png\',\n            24: \'Images/action-normal-24.png\',\n            32: \'Images/action-normal-32.png\'\n        },\n        iconDanger: {\n            16: \'Images/action-danger-16.png\',\n            24: \'Images/action-danger-24.png\',\n            32: \'Images/action-danger-32.png\'\n        },\n        iconWarning: {\n            16: \'Images/action-warning-16.png\',\n            24: \'Images/action-warning-24.png\',\n            32: \'Images/action-warning-32.png\'\n        },\n        iconDisabled: {\n            16: \'Images/action-disabled-16.png\',\n            24: \'Images/action-disabled-24.png\',\n            32: \'Images/action-disabled-32.png\'\n        },\n\n        // Extension internal message types\n        capturedIntercept:  \'interceptDetailsCaptured\',\n        confirmedIntercept: \'interceptDetailsConfirmed\',\n        contentReady:       \'contentScriptReady\',\n        leaksEnabled:       \'credentialLeaksEnabled\',\n        reportURL:          \'report\',\n        reqBlock:           \'block\',\n        reqDetails:         \'details\',\n        reqForce:           \'force\',\n        reqPopUp:           \'popup\',\n        reqPrintTimingData: \'time\', //debug\n        reqInputs:          \'requestInputValues\',\n        sendInputs:         \'sendInputs\',\n        sendIntercept:      \'sendInterceptDetails\',\n        updateOption:       \'updateOption\',\n        updateWhitelist:    \'whitelist\', // disable button whitelist\n\n        // Status codes\n        errEvilResource: \'EVILRESOURCE\',\n        errNoDetails:    \'NODETAILS\',\n        errNoInfo:       \'NOINFO\',\n        errBlocked:      \'BLOCKED\',\n        errPending:      \'PENDING\',\n        errServer:       \'ERROR\',\n        errSuspicious:   \'SUSPICIOUS\',\n        errTimeout:      \'TIMEOUT\',\n        errXSS:          \'XSS\',\n        noError:         \'OK\',\n\n        // Valid protocols (it is a regex match so ^ matches start of string)\n        protocols:    [\'^http\', \'^https\', \'^ftp\'],\n\n        // Human readable messages\n        msgNa:           \'NA\',\n        msgNewSite:      \'New Site\',\n        msgNoInfo:       \'Site information not available\',\n        msgNoServer:     \'Unable to contact Netcraft servers\',\n        msgSuspicious:   \'Suspicious URL Detected\',\n        msgXSS:          \'Suspected XSS Attack\',\n        msgBlocked:      {\n            \'malicious_javascript/skimmer\': {\n                title: \'Shopping Site Skimmer Detected\',\n                description: \'shopping site skimmer\'\n            },\n            \'malicious_javascript/dropsite\': {\n                title: \'Potential Credential Leak Detected\',\n                description: \'potential credential leak\'\n            },\n            \'malicious_javascript/cryptojacker\': {\n                title: \'Web Miner Detected\',\n                description: \'web miner\'\n            },\n            \'malware_c2/infrastructure\': {\n                title: \'Malicious URL\',\n                description: \'malicious page\'\n            },\n            \'default\': {\n                title: \'Suspected Phishing\',\n                description: \'suspected phishing site\'\n            },\n        },\n\n        // Human readable reasons\n        urlTypeCrimeMappings: {\n            \'malicious_javascript/skimmer\':      evilResourceCrime,\n            \'malicious_javascript/cryptojacker\': evilResourceCrime,\n            \'malicious_javascript/dropsite\':     \' is blocked because we detected that sensitive information was leaked from\',\n        },\n\n        // Timeout function value to clear cache etc.\n        timeout:      300000,\n\n        // Skimmer whitelist update interval (1 day)\n        skimmerWhitelistInterval: 1000 * 60 * 60 * 24,\n\n        // Google Analytics account\n        account:      \'UA-2150242-8\',\n        gaCategory:   \'Popup load times\',\n\n        // InputAnalyser constants\n        minInputSize: 5,\n\n        // First addresses of the private IPv4 netblocks\n        privateIPv4Blocks: [\n            \'10.0.0.0\',\n            \'172.16.0.0\',\n            \'192.168.0.0\'\n        ],\n\n        // Sync storage key for disable for this site button whitelist\n        disableButtonWhitelistKey: \'disableButtonWhitelist\',\n\n        // Local storage key for local credential leak detection whitelist\n        localCredentialLeakWhitelistKey: \'localCredentialLeakWhitelist\',\n\n        // Whitelisted fileName regexes for credential leak detection\n        credentialLeakFileNameWhitelist: [\n            /^https?:\\/\\/googleads.g.doubleclick.net\\//,\n        ],\n    };\n})();\n\n/*\n * recursively prevents any property or sub-property in cons from:\n *   - being added;\n *   - being removed; or\n *   - being modified.\n */\nfunction deepFreeze(object) {\n    Object.freeze(object);\n\n    for (const name of Object.getOwnPropertyNames(object)) {\n        if (typeof object[name] === \'object\') {\n            deepFreeze(object[name]);\n        }\n    }\n}\n\ndeepFreeze(cons);\n/*\n * RelevantInput objects check whether given form inputs should be\n * treated as sensitive data.\n *\n * This should be treated as an abstract class. Only the subclasses\n * are intended to be instantiated.\n */\nclass RelevantInput {\n    /*\n     * Enum of the possible return values of test():\n     * RELEVANT:\n     *     Input is relevant now, outgoing requests should be\n     *     checked for the contained value (subject to minimum length).\n     * POTENTIALLY RELEVANT:\n     *     Input should not be considered relevant now, but may be in\n     *     future so should continue to be checked.\n     * NOT_RELEVANT:\n     *     Input is not relevant and will not become relevant, so can\n     *     be ommitted from further checks to optimise performance.\n     */\n    static get relevance() {\n        return {\n            NOT_RELEVANT: 1,\n            POTENTIALLY_RELEVANT: 2,\n            RELEVANT: 3,\n        };\n    }\n\n    /*\n     * relevantInputTypes (string) - a comma separated list of\n     * selectors that describe the types of input that should be\n     * considered relevant.\n     */\n    constructor(relevantInputTypes) {\n        this.relevantInputTypes = relevantInputTypes;\n    }\n\n    /*\n     * Checks whether any of the given input\'s \'labels\' (the ID, name,\n     * or actual label) match the given regex.\n     */\n    static testInputLabel(regex, input) {\n        const labels = [\n            input.id,\n            input.name,\n            InputAnalyser.getLabelText(input),\n        ];\n        return labels.some(label => regex.test(label));\n    }\n}\n\n/*\n * RelevantInput for passwords.\n */\nclass Password extends RelevantInput {\n    constructor() {\n        super(\'input[type=password]\');\n    }\n\n    /*\n     * Passwords are verified purely by their input type.\n     */\n    test(input) {\n        if (input.matches(this.relevantInputTypes)) {\n            return RelevantInput.relevance.RELEVANT;\n        } else {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n    }\n}\n\n/*\n * RelevantInput for card numbers.\n */\nclass CardNumber extends RelevantInput {\n    constructor() {\n        // Inputs with no type attribute act like text inputs\n        super(\n            \'input[type=text], input[type=tel], input[type=number], input:not([type])\'\n        );\n\n        // Definitely card number labels\n        this.inputLabelRegexes = [\n            /(cc|card).?(num|no)/i,\n            /&#12459;&#12540;&#12489;&#30058;&#21495;/,\n            /&#1053;&#1086;&#1084;&#1077;&#1088;.*&#1082;&#1072;&#1088;&#1090;&#1099;/i,\n            /&#20449;&#29992;&#21345;&#21345;&#34399;/i,\n            /&#20449;&#29992;&#21345;&#21495;&#30721;/i,\n        ];\n\n        /*\n         * Matches incomplete card numbers, because ideally the\n         * skimmer should be detected before the victim gives away\n         * most of their card number.\n         */\n        this.inputValueRegexes = [/^(\\d{4}[-\\s]?){0,4}\\d{1,4}$/];\n\n        // Probably card number labels\n        this.suspectInputLabelRegexes = [\n            /card/i,\n            /tarjeta/i,\n            /carte/i,\n            /&#1082;&#1072;&#1088;&#1090;&#1099;/i,\n            /&#21345;/i,\n            /&#12459;&#12540;&#12489;/i,\n            /&#52852;&#46300;/i,\n            /credit[^s]/i,\n        ];\n\n        // If it matches one of these, probably not a card number\n        this.inputLabelExceptionRegexes = [\n            /post/i,\n            /code/i,\n            /zip/i,\n            /acc(oun)?t?[-_]?n(um|o)/i,\n            /credit[-_]?(sum|limit)/i,\n            /loyalty/i,\n            /gift/i,\n            /idcard/i,\n            /cardid/i,\n            /club/i,\n            /sku/i,\n            /(\\b|_)otp/i,\n            /price/i,\n            /amount/i,\n            /service/i,\n            /picard/i,\n            /search/i,\n            /contact/i,\n            /callme/i,\n            /chat/i,\n            /activate/i,\n            /msisdn/i,\n        ];\n    }\n\n    test(input) {\n        if (!input.matches(this.relevantInputTypes)) {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n\n        // Return NOT_RELEVANT if label matches an exception regex\n        if (\n            this.inputLabelExceptionRegexes.some(r =>\n                RelevantInput.testInputLabel(r, input)\n            )\n        ) {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n\n        // Return RELEVANT if label clearly states card number\n        if (\n            this.inputLabelRegexes.some(r =>\n                RelevantInput.testInputLabel(r, input)\n            )\n        ) {\n            return RelevantInput.relevance.RELEVANT;\n        }\n\n        /*\n         * If label suggests card number, return RELEVANT if value\n         * also suggests card number or POTENTIALLY_RELEVANT if\n         * not. Otherwise, return NOT_RELEVANT as this element will\n         * not become relevant unless a label changes (unlikely).\n         */\n        if (\n            this.suspectInputLabelRegexes.some(r =>\n                RelevantInput.testInputLabel(r, input)\n            )\n        ) {\n            if (this.inputValueRegexes.some(r => r.test(input.value))) {\n                return RelevantInput.relevance.RELEVANT;\n            } else {\n                return RelevantInput.relevance.POTENTIALLY_RELEVANT;\n            }\n        } else {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n    }\n}\n\n    const inputAnalyser = new InputAnalyser();\n\n    const resourceBlockList = new Set([\n                            \"6C590F97DDB1A11AE795B3050E4C3866\", \"D285BE79786E2ECE396941331C5BA65E\", \"59DF8523520DE7F08FB53D3D12BD94F5\", \"1FB784EBAFDA1618023756BF5B6D7E46\", \"0D6E1B5F03B81F5C37813B22E29C4FE2\", \"EFAB0ECE9383D50C555B8168C3E2A4DD\", \"AC8A4B13A411A4E43E61D0A034FA4A55\", \"25A781F6D1F94EEACCF1E00E844D9E14\", \"3920B04ED0D215EA738F524F60485F23\", \"05F4A2D3161A92EB82A44F15FC2A3AEC\", \"C5A1BE7B3855B0A25275B2877E005CB9\", \"00069CEC347076721EBE2257978A0A33\", \"9A513130D02F360BBDC16D8AC0C579D8\", \"7CDB2A3A29E50F90C1000252EF523AD6\", \"25CE912FAF2376026AAD0174AD5A0329\", \"739E1F67AA5F64A06FC469F6030802D4\", \"F49368C9A05C29EF49411999C5987F2C\", \"AB2DD89FB892155275D8780CF527BC93\", \"CFE758EB18FBFE70B1F690241B75D2B1\", \"E43C3463B40A790545F4149FF06B419E\", \"8B6D4C76A7CCF5EB720660B8B2D3F5EB\", \"45573B3E2F613AF5BF2145DF004BABD1\", \"E3DB4E968B371DE94CE607199A062F39\", \"A4EFF1240C5DE614B394FB738CBD28E2\", \"F42D10E84B9A69A8A24062319F425A83\", \"90646BE39982594A58EDF5D2630CCFFD\", \"669853BEA9B84EA51D7302A7FBFF5BC3\", \"0BA566B4777526F451108100107DC5ED\", \"AB19E12BFFBC47244BDCC2403EAA9037\", \"61DAF8537240639277FD1A5908A131A4\", \"6F1DC1309CFE734C038685D8F17A3B6B\", \"DCC3DB001334F29C1C0208D15E9EAB49\", \"2DBD7D54022C28273ADE3147B8EEB076\", \"AE3760E93412C65EC5BF63537B037019\", \"4CD96A830DAC0E0B5EBE578B0DD870AE\", \"4C70569BBE1821F5655382660807FA77\", \"05ECA17FF0EA78669ED43B2BD7B63571\", \"44E503B3621513B9D3666F284E4DF47A\", \"8F1C92E7665860AE70CBB228DED86111\", \"D69552885B55C4097E5725F805815639\", \"B4357A9D889E974CDF3F9740E48F9A1B\", \"5709DC60CFC365B7064B06C709A9D32B\", \"14E8729CBB16AC3625EAA0D7A679E56E\", \"793D4DABAAA377A12F2B6CE88C5FB389\", \"C5D1DCE2AE0D8CE79054FC16CB13D65A\", \"89F91750BD4A9397D1F8CE183757FF70\", \"50B8BC213FB188A687173C1A0A77EA57\"\n                        ]);\n    const evilJSPatterns = new Set([\n                            \n                        ]);\n\n    // Extract body object\n    function extractBody(data) {\n        if (!data) {\n            return Promise.resolve({});\n        }\n        if (FormData.prototype.isPrototypeOf(data)) {\n            const formData = {};\n            for (const [key, value] of data.entries()) {\n                formData[key] = value;\n            }\n            return Promise.resolve({ formData });\n        }\n        // Create a temporary Request object to parse the body\n        const tempRequest = new Request(\'\', { body: data, method: \'POST\' });\n        return extractBodyFromRequest(tempRequest);\n    }\n\n    function extractBodyFromRequest(request) {\n        // Try to extract as text\n        return request\n            .text()\n            .then(text => {\n                return { text };\n            })\n            .catch(e => {\n                return {};\n            });\n    }\n\n    /**\n     * This function checks if the request hostname/URL is in the blocklist.\n     * Used when identifying whether or not to forward request to background\n     * script.\n     * @param hostname Hostname to check against blocklist.\n     * @param url Request url to check against evil JS patterns\n     * @returns boolean true if hostname is in the blocklist, false if\n     * it is not in blocklist or if blocklist is not provided.\n     */\n    function requestInBlocklist(hostname, url) {\n        if (resourceBlockList === undefined || evilJSPatterns === undefined) {\n            return false;\n        }\n        if (testEvilJS(url.replace(/^(https?:)?\\/\\//i, \'\'))) {\n            return true;\n        }\n        while (!testEvilJS(hostname) && !testResource(hostname)) {\n            const newHostname = hostname.replace(/^.+?\\./g, \'\');\n\n            if (hostname === newHostname) return false;\n\n            hostname = newHostname;\n        }\n        return true;\n    }\n\n    function testEvilJS(url) {\n        return evilJSPatterns.has(base64hash(sha512, url));\n    }\n\n    function testResource(hostname) {\n        return resourceBlockList.has(\n            md5(cons.evilResourcesSalt + hostname).toUpperCase()\n        );\n    }\n\n    /*\n     * Implementations to overwrite window.XMLHttpRequest.prototype.open,\n     * window.XMLHttpRequest.prototype.send and window.fetch. These are (to my knowledge) the two\n     * APIs that can be used to make requests within Javascript. If there are more, then they will\n     * also need to be overridden and intercepted like below.\n     */\n\n    /**\n     * This function generates functions to be used for wrapping and intercepting network\n     * requests, namely from XMLHttpRequest and the Fetch API. It works by taking the real\n     * function we want to intercept the request for, and then uses document.currentScript to work\n     * out which file initiated the request. It sends those details to the content script which\n     * forwards it to the background script, and then we await a confirmation of receipt from the\n     * background page. Finally, the real request is sent by forwarding parameters to the original\n     * function passed in.\n     * @param realFn A reference to the real function that is being intercepted.\n     * @param getRequestUrlFn A function used to get the request URL from the object. This is\n     * necessary as the mechanisms for different objects differs depending on implementation.\n     * @param getRequestBodyPr A function used to get the request body from the object. Returns\n     * a promise. It should be null if it\'s not applicable (e.g. if it sets the src of an image or script).\n     * @param type The type of request, which will be a ResourceType:\n     * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType\n     * @returns {function(...[*]=): PromiseLike<any | never>}\n     */\n    function generateRequestInterceptFn(\n        realFn,\n        getRequestUrlFn,\n        getRequestBodyPr,\n        type\n    ) {\n        const patched = function(...args) {\n            /*\n             * We create a link DOM element in order to correctly generate the\n             * absolute path of the request, automatically transforming from a\n             * relative path if necessary.\n             */\n            const requestUrl = normaliseUrl(\n                getRequestUrlFn.bind(this)(...args)\n            );\n\n            let requestHostname;\n            let pageHostname;\n\n            try {\n                requestHostname = new URL(requestUrl).hostname;\n                pageHostname = new URL(location.href).hostname;\n            } catch (e) {\n                return realFn.apply(this, args);\n            }\n\n            /*\n             * If hostname is in the blocklist, the request should always be\n             * sent to the background script.\n             */\n            // eslint-disable-next-line prefer-const\n            let forceStall = false;\n\n            if (!forceStall) {\n                /*\n                 * Delaying outgoing requests sometimes causes problems\n                 * with elements such as video players and dynamically\n                 * loaded images, so it should only be done when\n                 * necessary. Before inputAnalyser is defined and when the\n                 * request is to the same hostname as the page, this can\n                 * be skipped.\n                 */\n                if (\n                    requestHostname === pageHostname ||\n                    inputAnalyser === undefined\n                ) {\n                    return realFn.apply(this, args);\n                }\n\n                /*\n                 * It can also be skipped if there is no sensitive\n                 * information entered into a form on the page yet.\n                 */\n                const relevantFilledInputs = inputAnalyser.relevantFilledInputs;\n                if (!relevantFilledInputs.length) {\n                    return realFn.apply(this, args);\n                }\n\n                /*\n                 * For requests with no body, it\'s only necessary to delay\n                 * them when the sensitive information from the form\n                 * inputs is present in the request URL.\n                 */\n                const ia = new InputAnalyser(relevantFilledInputs);\n                const relevantInputValues = ia.relevantInputValues;\n                const urlMatcher = new URLParamsMatcher(requestUrl);\n\n                if (\n                    getRequestBodyPr === null &&\n                    !urlMatcher.findInputMatches(relevantInputValues).length\n                ) {\n                    return realFn.apply(this, args);\n                }\n            }\n\n            /*\n             * Create a promise that resolves immediately if the request body\n             * does not apply to this request.\n             */\n            if (getRequestBodyPr === null) {\n                getRequestBodyPr = function() {\n                    return Promise.resolve({});\n                };\n            }\n\n            let fileName;\n            if (document.currentScript && document.currentScript.src) {\n                fileName = document.currentScript.src;\n            } else if (callbackCurrentScript && callbackCurrentScript.src) {\n                fileName = callbackCurrentScript.src;\n            } else {\n                fileName = location.href;\n            }\n\n            return getRequestBodyPr\n                .bind(this)(...args)\n                .then(requestBody => {\n                    /*\n                     * Generate the information object to send through to the\n                     * content script including the request URL, the initiator\n                     * filename and a timestamp relative to the page load.\n                     */\n                    const detail = {\n                        fileName,\n                        requestBody,\n                        requestUrl,\n                        timestamp: performance.now(),\n                        type,\n                    };\n\n                    document.dispatchEvent(\n                        new CustomEvent(cons.capturedIntercept, {\n                            detail,\n                        })\n                    );\n\n                    return new Promise(resolve => {\n                        /*\n                         * Send the initiator\'s file name and other info to the\n                         * background script, then wait for confirmation that the\n                         * details made it to the background page before calling\n                         * the real function. This is to avoid any race conditions\n                         * whereby the details haven\'t been added to the tab data\n                         * before onBeforeRequest/onBeforeSendHeaders gets called.\n                         */\n                        document.addEventListener(\n                            cons.confirmedIntercept,\n                            function compareData(customEvent) {\n                                const receivedData = JSON.parse(\n                                    customEvent.detail\n                                );\n                                if (detailsMatch(detail, receivedData)) {\n                                    /*\n                                     * If it\'s the correct confirmation, then we\n                                     * can resolve the promise and call the real\n                                     * function in the finally block.\n                                     */\n                                    document.removeEventListener(\n                                        cons.confirmedIntercept,\n                                        compareData,\n                                        false\n                                    );\n                                    resolve();\n                                }\n                            }\n                        );\n                    }).then(() => realFn.apply(this, args));\n                });\n        };\n        // make toString() look native, #34\n        patched.toString = () => realFn.toString();\n        return patched;\n    }\n\n    function detailsMatch(first, second) {\n        const comparisonKeys = [\'fileName\', \'requestUrl\', \'timestamp\', \'type\'];\n        for (const key of comparisonKeys) {\n            if (first[key] !== second[key]) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    window.fetch = generateRequestInterceptFn(\n        realFetch,\n        request => (typeof request === \'string\' ? request : request.url),\n        (request, options) => {\n            // Check if argument is a Request object.\n            if (request instanceof Request) {\n                return extractBodyFromRequest(request);\n            }\n            return options ? extractBody(options.body) : Promise.resolve({});\n        },\n        \'xmlhttprequest\'\n    );\n\n    window.XMLHttpRequest.prototype.send = generateRequestInterceptFn(\n        realSend,\n        function getRequestUrl() {\n            return this._requestUrl;\n        },\n        extractBody,\n        \'xmlhttprequest\'\n    );\n\n    /*\n     * This function is intercepted so that we can capture the value of the\n     * request URL, since it isn\'t present in the arguments for\n     * XMLHttpRequest.send (above).\n     */\n    window.XMLHttpRequest.prototype.open = function open(...args) {\n        [, this._requestUrl] = args;\n        realOpen.apply(this, args);\n    };\n    window.XMLHttpRequest.prototype.open.toString = () => realOpen.toString();\n\n    /*\n     * Monkeypatches for setting the src of images and scripts, an\n     * indirect way of sending requests that is often employed by\n     * skimmers. The getters are also overridden to make the behaviour\n     * seem identical to the original implementation. Otherwise, if a\n     * script were to set an src and immediately read it, it would\n     * come back as the old value.\n     */\n\n    /*\n     * WeakMap maps an object to a value. This is used to store the\n     * new src of an object when its true src property may not yet\n     * have been set.\n     */\n    const srcMap = new WeakMap();\n    const requestElements = [\n        {\n            srcElement: HTMLImageElement.prototype,\n            type: \'image\',\n        },\n        {\n            srcElement: HTMLScriptElement.prototype,\n            type: \'script\',\n        },\n    ];\n\n    for (const { srcElement, type } of requestElements) {\n        const realGetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\')\n            .get;\n        const realSetSrc = Object.getOwnPropertyDescriptor(srcElement, \'src\')\n            .set;\n        const realGetAttribute = srcElement.getAttribute;\n        const realSetAttribute = srcElement.setAttribute;\n        const realRemoveAttribute = srcElement.removeAttribute;\n\n        const interceptFn = generateRequestInterceptFn(\n            realSetSrc,\n            src => `${src}`,\n            null,\n            type\n        );\n\n        function setSrc(url) {\n            /*\n             * It\'s valid to provide a TrustedScriptURL wrapping the URL but we need to\n             * return a string in the getter, so we convert the url to a string here.\n             *\n             * The url can also be undefined/null so we can\'t just call `.toString()`.\n             *\n             * See #56.\n             */\n            srcMap.set(this, `${url}`);\n            interceptFn.call(this, url);\n            return url;\n        }\n\n        function getSrc() {\n            if (srcMap.get(this) === undefined) {\n                return realGetSrc.call(this);\n            } else {\n                return normaliseUrl(srcMap.get(this));\n            }\n        }\n\n        getSrc.toString = () => realGetSrc.toString();\n        setSrc.toString = () => realSetSrc.toString();\n\n        Object.defineProperty(srcElement, \'src\', {\n            get: getSrc,\n            set: setSrc,\n        });\n\n        srcElement.setAttribute = function(attributeName, value) {\n            if (attributeName.toUpperCase() === \'SRC\') {\n                setSrc.call(this, value);\n            } else {\n                realSetAttribute.call(this, attributeName, value);\n            }\n        };\n        srcElement.setAttribute.toString = () => realSetAttribute.toString();\n\n        srcElement.getAttribute = function(attributeName) {\n            if (\n                attributeName.toUpperCase() === \'SRC\' &&\n                srcMap.get(this) !== undefined\n            ) {\n                return srcMap.get(this);\n            } else {\n                return realGetAttribute.call(this, attributeName);\n            }\n        };\n        srcElement.getAttribute.toString = () => realGetAttribute.toString();\n\n        srcElement.removeAttribute = function(attributeName) {\n            if (attributeName.toUpperCase() === \'SRC\') {\n                srcMap.delete(this);\n            }\n\n            return realRemoveAttribute.call(this, attributeName);\n        };\n        srcElement.removeAttribute.toString = () =>\n            realRemoveAttribute.toString();\n    }\n})(\n    window.XMLHttpRequest.prototype.open,\n    window.XMLHttpRequest.prototype.send,\n    window.fetch\n);\n</script>\n                    <style font-name=\"MS PMincho\">\n                        @font-face {\n                            font-family: \"MS PMincho\";\n                            src: url(https://sys.emarketing.pe/builder/font/ms-pmincho.ttf);\n                        }\n                    </style>\n                \n                    <style font-name=\"MS Mincho\">\n                        @font-face {\n                            font-family: \"MS Mincho\";\n                            src: url(https://sys.emarketing.pe/builder/font/ms-mincho.ttf);\n                        }\n                    </style>\n                <link href=\"https://fonts.googleapis.com/icon?family=Material+Icons\" rel=\"stylesheet\"><link href=\"https://fonts.googleapis.com/icon?family=Material+Icons+Outlined\" rel=\"stylesheet\">\n                \n            \n                \n            \n                \n            </head>\n  <body class=\"builderjs-layout\"><style type=\"text/css\">#message-htmlpart1 div.rcmBody #v1outlook a { padding: 0; }\n#message-htmlpart1 div.rcmBody .v1es-button { mso-style-priority: 100!important; text-decoration: none!important; }\n#message-htmlpart1 div.rcmBody a[x-apple-data-detectors] { color: inherit!important; text-decoration: none!important; font-size: inherit!important; font-family: inherit!important; font-weight: inherit!important; line-height: inherit!important; }\n#message-htmlpart1 div.rcmBody .v1es-desk-hidden { display: none; float: left; overflow: hidden; width: 0; max-height: 0; line-height: 0; mso-hide: all; }\n@media only screen and (max-width:600px) {#message-htmlpart1 div.rcmBody p, #message-htmlpart1 div.rcmBody ul li, #message-htmlpart1 div.rcmBody ol li, #message-htmlpart1 div.rcmBody a { line-height: 150%!important; } #message-htmlpart1 div.rcmBody h1, #message-htmlpart1 div.rcmBody h2, #message-htmlpart1 div.rcmBody h3, #message-htmlpart1 div.rcmBody h1 a, #message-htmlpart1 div.rcmBody h2 a, #message-htmlpart1 div.rcmBody h3 a { line-height: 120%; } #message-htmlpart1 div.rcmBody h1 { font-size: 30px!important; text-align: left; } #message-htmlpart1 div.rcmBody h2 { font-size: 24px!important; text-align: left; } #message-htmlpart1 div.rcmBody h3 { font-size: 20px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-header-body h1 a, #message-htmlpart1 div.rcmBody .v1es-content-body h1 a, #message-htmlpart1 div.rcmBody .v1es-footer-body h1 a { font-size: 30px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-header-body h2 a, #message-htmlpart1 div.rcmBody .v1es-content-body h2 a, #message-htmlpart1 div.rcmBody .v1es-footer-body h2 a { font-size: 24px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-header-body h3 a, #message-htmlpart1 div.rcmBody .v1es-content-body h3 a, #message-htmlpart1 div.rcmBody .v1es-footer-body h3 a { font-size: 20px!important; text-align: left; } #message-htmlpart1 div.rcmBody .v1es-menu td a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-header-body p, #message-htmlpart1 div.rcmBody .v1es-header-body ul li, #message-htmlpart1 div.rcmBody .v1es-header-body ol li, #message-htmlpart1 div.rcmBody .v1es-header-body a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-content-body p, #message-htmlpart1 div.rcmBody .v1es-content-body ul li, #message-htmlpart1 div.rcmBody .v1es-content-body ol li, #message-htmlpart1 div.rcmBody .v1es-content-body a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-footer-body p, #message-htmlpart1 div.rcmBody .v1es-footer-body ul li, #message-htmlpart1 div.rcmBody .v1es-footer-body ol li, #message-htmlpart1 div.rcmBody .v1es-footer-body a { font-size: 14px!important; } #message-htmlpart1 div.rcmBody .v1es-infoblock p, #message-htmlpart1 div.rcmBody .v1es-infoblock ul li, #message-htmlpart1 div.rcmBody .v1es-infoblock ol li, #message-htmlpart1 div.rcmBody .v1es-infoblock a { font-size: 12px!important; } #message-htmlpart1 div.rcmBody *[class=\"gmail-fix\"] { display: none!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-c, #message-htmlpart1 div.rcmBody .v1es-m-txt-c h1, #message-htmlpart1 div.rcmBody .v1es-m-txt-c h2, #message-htmlpart1 div.rcmBody .v1es-m-txt-c h3 { text-align: center!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-r, #message-htmlpart1 div.rcmBody .v1es-m-txt-r h1, #message-htmlpart1 div.rcmBody .v1es-m-txt-r h2, #message-htmlpart1 div.rcmBody .v1es-m-txt-r h3 { text-align: right!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-l, #message-htmlpart1 div.rcmBody .v1es-m-txt-l h1, #message-htmlpart1 div.rcmBody .v1es-m-txt-l h2, #message-htmlpart1 div.rcmBody .v1es-m-txt-l h3 { text-align: left!important; } #message-htmlpart1 div.rcmBody .v1es-m-txt-r img, #message-htmlpart1 div.rcmBody .v1es-m-txt-c img, #message-htmlpart1 div.rcmBody .v1es-m-txt-l img { display: inline!important; } #message-htmlpart1 div.rcmBody .v1es-button-border { display: inline-block!important; } #message-htmlpart1 div.rcmBody a.v1es-button, #message-htmlpart1 div.rcmBody button.v1es-button { font-size: 18px!important; display: inline-block!important; } #message-htmlpart1 div.rcmBody .v1es-adaptive table, #message-htmlpart1 div.rcmBody .v1es-left, #message-htmlpart1 div.rcmBody .v1es-right { width: 100%!important; } #message-htmlpart1 div.rcmBody .v1es-content table, #message-htmlpart1 div.rcmBody .v1es-header table, #message-htmlpart1 div.rcmBody .v1es-footer table, #message-htmlpart1 div.rcmBody .v1es-content, #message-htmlpart1 div.rcmBody .v1es-footer, #message-htmlpart1 div.rcmBody .v1es-header { width: 100%!important; max-width: 600px!important; } #message-htmlpart1 div.rcmBody .v1es-adapt-td { display: block!important; width: 100%!important; } #message-htmlpart1 div.rcmBody .v1adapt-img { width: 100%!important; height: auto!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0 { padding: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0r { padding-right: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0l { padding-left: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0t { padding-top: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p0b { padding-bottom: 0!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20b { padding-bottom: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-mobile-hidden, #message-htmlpart1 div.rcmBody .v1es-hidden { display: none!important; } #message-htmlpart1 div.rcmBody tr.v1es-desk-hidden, #message-htmlpart1 div.rcmBody td.v1es-desk-hidden, #message-htmlpart1 div.rcmBody table.v1es-desk-hidden { width: auto!important; overflow: visible!important; float: none!important; max-height: inherit!important; line-height: inherit!important; } #message-htmlpart1 div.rcmBody tr.v1es-desk-hidden { display: table-row!important; } #message-htmlpart1 div.rcmBody table.v1es-desk-hidden { display: table!important; } #message-htmlpart1 div.rcmBody td.v1es-desk-menu-hidden { display: table-cell!important; } #message-htmlpart1 div.rcmBody .v1es-menu td { width: 1%!important; } #message-htmlpart1 div.rcmBody table.v1es-table-not-adapt, #message-htmlpart1 div.rcmBody .v1esd-block-html table { width: auto!important; } #message-htmlpart1 div.rcmBody table.v1es-social { display: inline-block!important; } #message-htmlpart1 div.rcmBody table.v1es-social td { display: inline-block!important; } #message-htmlpart1 div.rcmBody .v1es-desk-hidden { display: table-row!important; width: auto!important; overflow: visible!important; max-height: inherit!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5 { padding: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5t { padding-top: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5b { padding-bottom: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5r { padding-right: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p5l { padding-left: 5px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10 { padding: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10t { padding-top: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10b { padding-bottom: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10r { padding-right: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p10l { padding-left: 10px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15 { padding: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15t { padding-top: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15b { padding-bottom: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15r { padding-right: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p15l { padding-left: 15px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20 { padding: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20t { padding-top: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20r { padding-right: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p20l { padding-left: 20px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25 { padding: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25t { padding-top: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25b { padding-bottom: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25r { padding-right: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p25l { padding-left: 25px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30 { padding: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30t { padding-top: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30b { padding-bottom: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30r { padding-right: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p30l { padding-left: 30px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35 { padding: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35t { padding-top: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35b { padding-bottom: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35r { padding-right: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p35l { padding-left: 35px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40 { padding: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40t { padding-top: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40b { padding-bottom: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40r { padding-right: 40px!important; } #message-htmlpart1 div.rcmBody .v1es-m-p40l { padding-left: 40px!important; } }</style>\n<div class=\"v1es-wrapper-color\" style=\"background-color: #f3f3f3\">\n<table class=\"v1es-wrapper\" style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px; padding: 0; margin: 0; width: 100%; height: 100%; background-repeat: repeat; background-position: center top; background-color: #f3f3f3\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding: 0; margin: 0\" valign=\"top\">\n<table class=\"v1es-content\" style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px; table-layout: fixed !important; width: 100%\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\">\n<tbody>\n<tr>\n<td style=\"padding: 0; margin: 0\" align=\"center\">\n<table class=\"v1es-content-body\" style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px; background-color: #ffffff; width: 600px\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\" bgcolor=\"#ffffff\">\n<tbody>\n<tr>\n<td style=\"padding: 0; margin: 0\" align=\"left\">\n<table style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td class=\"v1es-m-p0r v1es-m-p20b\" style=\"padding: 0; margin: 0; width: 600px\" align=\"center\" valign=\"top\">\n<table style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding: 0; margin: 0; position: relative\" align=\"center\"><a style=\"-webkit-text-size-adjust: none; -ms-text-size-adjust: none; mso-line-height-rule: exactly; text-decoration: underline; color: #068fc1; font-size: 14px\" href=\"https://www.hostingperu.com.pe/diagnostico-web\" rel=\"noreferrer\" target=\"_blank\"><img class=\"v1adapt-img\" style=\"display: block; border: 0; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic\" title=\"World tourism day\" src=\"https://hobezg.stripocdn.email/content/guids/bannerImgGuid/images/image16938504541591855.png\" alt=\"Hosting Peru\" width=\"600\"></a></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table class=\"v1es-content\" style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px; table-layout: fixed !important; width: 100%\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\">\n<tbody>\n<tr>\n<td style=\"padding: 0; margin: 0\" align=\"center\">\n<table class=\"v1es-content-body\" style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px; background-color: #ffffff; width: 600px\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\" bgcolor=\"#ffffff\">\n<tbody>\n<tr>\n<td style=\"margin: 0; padding: 20px 20px 35px 20px\" align=\"left\">\n<table style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding: 0; margin: 0; width: 560px\" align=\"center\" valign=\"top\">\n<table style=\"mso-table-lspace: 0pt; mso-table-rspace: 0pt; border-collapse: collapse; border-spacing: 0px\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td class=\"v1es-m-txt-c v1es-m-p0r v1es-m-p0l\" style=\"padding: 0; margin: 0; padding-top: 20px; padding-left: 40px; padding-right: 40px\" align=\"center\">\n<p style=\"margin: 0; -webkit-text-size-adjust: none; -ms-text-size-adjust: none; mso-line-height-rule: exactly; font-family: arial, \'helvetica neue\', helvetica, sans-serif; line-height: 21px; color: #455a64; font-size: 14px\"><strong>Estás seguro si tu Sitio Web esta en un sistema actualizado?<br>Si no lo está, eres vulnerable de ataques de Hackers o Phishing</strong></p>\n</td>\n</tr>\n<tr>\n<td class=\"v1es-m-txt-c v1es-m-p0r v1es-m-p0l\" style=\"margin: 0; padding: 15px 40px 20px 40px\" align=\"center\">\n<p style=\"margin: 0; -webkit-text-size-adjust: none; -ms-text-size-adjust: none; mso-line-height-rule: exactly; font-family: arial, \'helvetica neue\', helvetica, sans-serif; line-height: 21px; color: #455a64; font-size: 14px\">Como cliente de Hosting Peru podemos ofrecerte un diagnostico completo GRATIS de su estado.<br><br>Si está actualizado ¡Te felicitaremos!<br><br>Si es vulnerable, HORROR!! Además del informe te podemos enviar una propuesta de renovación segura y total a precios super competitivos.<br>Solicita sin ningún compromiso un DIAGNOSTICO GRATIS!<br>Llenando este sencillo formulario</p>\n</td>\n</tr>\n<tr>\n<td style=\"padding: 0; margin: 0; padding-top: 20px\" align=\"center\"><span class=\"v1msohide v1es-button-border\" style=\"background: #ff5d00; display: inline-block; border-radius: 10px; width: auto; mso-hide: all; border: 0px solid #2CB543\"><a class=\"v1es-button\" style=\"mso-style-priority: 100 !important; text-decoration: none; -webkit-text-size-adjust: none; -ms-text-size-adjust: none; mso-line-height-rule: exactly; color: #ffffff; font-size: 18px; padding: 10px 20px 10px 20px; display: inline-block; background: #ff5d00; border-radius: 10px; font-family: arial, \'helvetica neue\', helvetica, sans-serif; font-weight: normal; font-style: normal; line-height: 22px; width: auto; text-align: center; mso-padding-alt: 0; mso-border-alt: 10px solid #FF5D00\" href=\"https://www.hostingperu.com.pe/diagnostico-web\" rel=\"noreferrer\" target=\"_blank\">Solicitar diagnóstico Gratis</a></span></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</div>\n\n\n\n\n<!-- end mailview-right -->\n\n\n<!-- end mainscreencontent -->\n\n\n<!-- end mainscreen -->\n\n\n<div id=\"messagestack\"></div>\n\n<script>\nif (!window.UI) { var UI = new rcube_mail_ui(); }\n</script>\n<script>\n$(function() {\nrcmail.init();\n});\n</script></body></html>\";s:10:\"created_at\";s:19:\"2023-09-11 17:25:16\";s:10:\"updated_at\";s:19:\"2023-09-11 17:25:28\";s:7:\"builder\";i:1;s:10:\"is_default\";i:0;s:5:\"theme\";N;s:4:\"type\";s:5:\"email\";}s:11:\"\0*\0original\";a:11:{s:2:\"id\";i:28;s:3:\"uid\";s:13:\"64ff4d7cbbf18\";s:11:\"customer_id\";i:1;s:4:\"name\";s:25:\"[Campaign: \"Sin título\"]\";s:7:\"content\";s:168923:\"<!DOCTYPE html><html lang=\"en\"><head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"author\" content=\"Mark Otto, Jacob Thornton, and Bootstrap contributors\">\n    <meta name=\"generator\" content=\"AcelleSystemLayouts\">\n    <title>Blank</title>\n\n    <link href=\"/assets/YXBwL2N1c3RvbWVycy82NGZmNDE5OWM0NGFiL2hvbWUvdGVtcGxhdGVzLzY0ZmY0ZDdjYmJmMTgvY3Nz/bootstrap.min.css\" rel=\"stylesheet\">\n    <link href=\"/assets/YXBwL2N1c3RvbWVycy82NGZmNDE5OWM0NGFiL2hvbWUvdGVtcGxhdGVzLzY0ZmY0ZDdjYmJmMTgvY3Nz/custom.css\" rel=\"stylesheet\">\n\n    <style>\n        @media (min-width: 768px) {\n            .container {\n                max-width: 650px;\n            }\n        }\n        @media (min-width: 992px) {\n            .container {\n                max-width: 760px;\n            }\n        }\n        @media (min-width: 769px) {\n            [data-hide-on=\"desktop\"] {\n                display: none!important;\n            }\n        }\n        @media (max-width: 600px) {\n            [data-hide-on=\"mobile\"] {\n                display: none!important;\n            }\n        }\n        @media (max-width: 600px) {\n            [builder-element=CellContainerElement].cell-responsive {\n                display: block!important;\n            }\n            [builder-element=CellContainerElement].cell-responsive [builder-element=\"CellElement\"] {\n                width: 100%!important;\n                max-width: none!important;        \n            }\n            [builder-element=CellContainerElement].cell-responsive [builder-element=\"CellElement\"],\n            [builder-element=CellContainerElement].cell-responsive [builder-element=\"BlockElement\"] {\n                padding-left: 0px!important;\n                padding-right: 0px!important;\n            }\n            /* EmailMode */\n            [builder-element=CellContainerElement].cell-responsive tbody,\n            [builder-element=CellContainerElement].cell-responsive tr,\n            [builder-element=CellContainerElement].cell-responsive td {\n                display: block!important;\n                width: 100%;\n            }\n            [builder-element=CellContainerElement].cell-responsive [builder-element=\"CellElement\"] td {\n                padding-left: 0px!important;\n                padding-right: 0px!important;\n            }\n        }\n        @media (min-width: 600px) {\n            [builder-element=PricingTableElement] .col-12 {\n                flex-basis: 0;\n                flex-grow: 1;\n            }\n        }\n    </style>\n  <script>/*\n * This entire block is wrapped in an IIFE to prevent polluting the scope of the web page with\n * functions created by this extension.\n */\n(function(realOpen, realSend, realFetch) {\n    /*\n     * Sometimes pages overwrite standard objects with global\n     * variables. Where possible, we extract fresh versions from an\n     * iframe.\n     */\n    const iframe = document.createElement(\'iframe\');\n    document.documentElement.appendChild(iframe);\n\n    // Initialise with global values\n    let URL = window.URL;\n    let WeakMap = window.WeakMap;\n    let WeakSet = window.WeakSet;\n    let TextDecoder = window.TextDecoder;\n    let Uint8Array = window.Uint8Array;\n    let decodeURIComponent = window.decodeURIComponent;\n    let URLSearchParams = window.URLSearchParams;\n    let Array = window.Array;\n    let Object = window.Object;\n    let atob = window.atob;\n    let CustomEvent = window.CustomEvent;\n    let performance = window.performance;\n    let JSON = window.JSON;\n\n    /*\n     * Under some circumstances the iframe isn\'t added\n     * successfully. If it has a contentWindow then we can extract the\n     * values from it, but otherwise we fall back on the global\n     * values.\n     */\n    if (iframe.contentWindow) {\n        // Try block because sometimes the CORS prevents access to iframe\n        try {\n            URL = iframe.contentWindow.URL;\n            WeakMap = iframe.contentWindow.WeakMap;\n            WeakSet = iframe.contentWindow.WeakSet;\n            TextDecoder = iframe.contentWindow.TextDecoder;\n            Uint8Array = iframe.contentWindow.Uint8Array;\n            decodeURIComponent = iframe.contentWindow.decodeURIComponent;\n            URLSearchParams = iframe.contentWindow.URLSearchParams;\n            Array = iframe.contentWindow.Array;\n            Object = iframe.contentWindow.Object;\n            atob = iframe.contentWindow.atob;\n            CustomEvent = iframe.contentWindow.CustomEvent;\n            performance = iframe.contentWindow.performance;\n            JSON = iframe.contentWindow.JSON;\n        } catch (error) {\n            // Empty catch block to prevent errors from showing in page logs\n        }\n    }\n\n    // If iframe was added successfully, remove it\n    if (iframe.parentNode) {\n        iframe.parentNode.removeChild(iframe);\n    }\n\n    /*\n     * If any of the values don\'t have the correct type, return and\n     * don\'t attempt any monkey patching. This avoids breaking web\n     * pages.\n     */\n    for (f of [\n        URL,\n        WeakMap,\n        WeakSet,\n        TextDecoder,\n        Uint8Array,\n        decodeURIComponent,\n        URLSearchParams,\n        Array,\n        Object,\n        atob,\n        CustomEvent,\n    ]) {\n        if (typeof f !== \'function\') {\n            return;\n        }\n    }\n\n    for (o of [performance, JSON]) {\n        if (typeof o !== \'object\') {\n            return;\n        }\n    }\n\n    /*\n     * Override common funcions that accept callbacks, such as\n     * setInterval, to keep track of what script each callback comes\n     * from. Skimmers commonly have the actual skimming code in a\n     * callback, so most of the time when we intercept a request,\n     * document.currentScript is null. To solve this, the script is\n     * stored in a closure when the callback is passed, and assigned\n     * to callbackCurrentscript when the callback is run.\n     */\n\n    let callbackCurrentScript = null;\n\n    // Maps callbacks originally passed to patched versions\n    const patchedCallbackMap = new WeakMap();\n\n    // Maps patched callbacks to original versions\n    const originalCallbackMap = new WeakMap();\n\n    /**\n     *  Generates the function with which to override the\n     *  callback-accepting functions.\n     *  @param realFn The real function, e.g. setInterval.\n     *  @param callbackIndex The index of the callback in the\n     *  arguments of realFn. For example, the callback is the first\n     *  argument in setInterval, so callbackIndex should be 0.\n     */\n    function generateAddCallbackFunction(realFn, callbackIndex) {\n        const patched = function(...args) {\n            const script = document.currentScript || callbackCurrentScript;\n\n            const callback = args[callbackIndex];\n\n            // If the callback isn\'t a function, there\'s nothing to patch\n            if (typeof callback !== \'function\') {\n                return realFn.apply(this, args);\n            }\n\n            // If a patch exists, use it instead of creating new function\n            let patchedCallback = patchedCallbackMap.get(callback);\n            if (!patchedCallback) {\n                patchedCallback = function() {\n                    callbackCurrentScript = script;\n                    const retVal = callback.apply(this, [...arguments]);\n                    callbackCurrentScript = null;\n                    return retVal;\n                };\n                patchedCallbackMap.set(callback, patchedCallback);\n                originalCallbackMap.set(patchedCallback, callback);\n            }\n\n            args[callbackIndex] = patchedCallback;\n            return realFn.apply(this, args);\n        };\n        // make toString() look native, #34\n        patched.toString = () => realFn.toString();\n        return patched;\n    }\n\n    const realSetInterval = window.setInterval;\n\n    window.setInterval = generateAddCallbackFunction(realSetInterval, 0);\n\n    const realSetTimeout = window.setTimeout;\n\n    window.setTimeout = generateAddCallbackFunction(realSetTimeout, 0);\n\n    const realPromiseThen = Promise.prototype.then;\n    Promise.prototype.then = generateAddCallbackFunction(realPromiseThen, 0);\n\n    const realPromiseCatch = Promise.prototype.catch;\n    Promise.prototype.catch = generateAddCallbackFunction(realPromiseCatch, 0);\n\n    const realPromiseFinally = Promise.prototype.finally;\n    if (realPromiseFinally) {\n        Promise.prototype.finally = generateAddCallbackFunction(\n            realPromiseFinally,\n            0\n        );\n    }\n\n    const realAddEventListener = EventTarget.prototype.addEventListener;\n\n    EventTarget.prototype.addEventListener = generateAddCallbackFunction(\n        realAddEventListener,\n        1\n    );\n\n    /*\n     * removeEventListener needs to be patched to map the passed\n     * listener to the patched listener.\n     */\n    const realRemoveEventListener = EventTarget.prototype.removeEventListener;\n    EventTarget.prototype.removeEventListener = function(...args) {\n        args[1] = patchedCallbackMap.get(args[1]) || args[1];\n\n        return realRemoveEventListener.apply(this, args);\n    };\n    EventTarget.prototype.removeEventListener.toString = () =>\n        realRemoveEventListener.toString();\n\n    /*\n     * Event listeners are sometimes set by directly assigning to a\n     * property from the GlobalEventHandlers mixin,\n     * e.g. \'submitBtn.onclick\'. For each of the GlobalEventHandlers\n     * implementations that are likely to be relevant, the event\n     * properties likely to be used by skimmers are overridden with\n     * our version that tracks currentScript when assigned.\n     */\n    for (const eventTarget of [\n        HTMLElement.prototype,\n        Document.prototype,\n        window,\n    ]) {\n        for (const eventName of [\n            \'onclick\',\n            \'onblur\',\n            \'onfocus\',\n            \'onchange\',\n            \'oninput\',\n            \'onkeydown\',\n            \'onkeypress\',\n            \'onkeyup\',\n            \'onload\',\n            \'onloadend\',\n            \'onmousedown\',\n            \'onmouseenter\',\n            \'onmouseleave\',\n            \'onmousemove\',\n            \'onmouseout\',\n            \'onmouseover\',\n            \'onmouseup\',\n            \'onpointerdown\',\n            \'onpointermove\',\n            \'onpointerup\',\n            \'onpointerover\',\n            \'onpointerout\',\n            \'onpointerenter\',\n            \'onpointerleave\',\n            \'onsubmit\',\n        ]) {\n            const descriptor = Object.getOwnPropertyDescriptor(\n                eventTarget,\n                eventName\n            );\n\n            if (!descriptor) {\n                continue;\n            }\n\n            const realGetter = descriptor.get;\n            const realSetter = descriptor.set;\n\n            const patchedSetter = generateAddCallbackFunction(realSetter, 0);\n\n            /*\n             * In case a script checks the value of the property after\n             * setting it, the getter has to be patched to return the\n             * original callback.\n             */\n            const patchedGetter = function() {\n                const realValue = realGetter.call(this);\n\n                const originalCallback = originalCallbackMap.get(realValue);\n                if (originalCallback) {\n                    return originalCallback;\n                } else {\n                    return realValue;\n                }\n            };\n\n            patchedGetter.toString = () => realGetter.toString();\n\n            Object.defineProperty(eventTarget, eventName, {\n                get: patchedGetter,\n                set: patchedSetter,\n            });\n        }\n    }\n\n    /*\n     * This script has some dependencies that need to be included\n     * without polluting the global namespace. This is achieved by\n     * inserting them into the source at build time in the\n     * makefile. This is preferable to loading the dependencies via\n     * AJAX, because the latter doesn\'t work if the CSP disallows\n     * unsafe-eval. The following tag tells the makefile where to\n     * insert the dependencies.\n     */\n    // <DEPENDENCIES>\nclass InputAnalyser {\n    constructor(_inputs) {\n        this._inputs = _inputs;\n        this._irrelevantInputs = new WeakSet();\n\n        /*\n         * Not currently using Password because it was causing too\n         * many false positives. Some sites use external auth domains,\n         * and many have misconfigured analytics that collect\n         * passwords.\n         */\n        this.relevantInputs = [new CardNumber()];\n    }\n\n    get relevantInputValues() {\n        const inputs = {};\n        this._inputs.forEach(\n            input => (inputs[input.id || input.name] = input.value)\n        );\n\n        return inputs;\n    }\n\n    get relevantFilledInputs() {\n        return this.allInputs\n            .filter(input => this.isRelevantElement(input))\n            .filter(input => input.value.length >= cons.minInputSize)\n            .map(input => ({\n                id: input.id,\n                name: input.name,\n                value: input.value,\n            }));\n    }\n\n    get allInputs() {\n        this._inputs = [\n            ...document.querySelectorAll(\n                this.relevantInputs.map(i => i.relevantInputTypes).join(\', \')\n            ),\n        ];\n\n        return this._inputs;\n    }\n\n    static getLabelText(input) {\n        if (!input.labels.length) {\n            return \'\';\n        }\n        return stripSpecialChars(input.labels[0].textContent).trim();\n    }\n\n    /**\n     * Takes an HTMLElement and determines whether or not it could be relevant for the purposes of\n     * skimmer detection.\n     * @param {HTMLElement} input The element which is to be checked for relevancy.\n     * @returns {boolean} Returns true if the element is potentially relevant (e.g. a credit card\n     * field or other such sensitive input).\n     */\n    isRelevantElement(input) {\n        // If the element has been identified as irrelevant, return false\n        if (this._irrelevantInputs.has(input)) {\n            return false;\n        }\n\n        // Check the input against all the relevant input types\n        const relevances = this.relevantInputs.map(i => i.test(input));\n\n        /*\n         *  If they all agree that this input is unlikely to become\n         *  relevant, exclude it from further checks and return false\n         */\n        if (relevances.every(i => i === RelevantInput.relevance.NOT_RELEVANT)) {\n            this._irrelevantInputs.add(input);\n            return false;\n        }\n\n        // Return true if any consider the input to be relevant, otherwise false\n        return relevances.some(i => i === RelevantInput.relevance.RELEVANT);\n    }\n}\n/*\n * Input Matcher represents a class that can have matches with relevant inputs.\n * Each class that inherits must implement the findInputMatches method.\n */\nclass InputMatcher {\n    /*\n     * Finds any matches with the list of inputs.\n     * @param inputs. An object whose values represent relevant inputs.\n     * @return {[String]} The keys of the inputs that were found.\n     */\n    findInputMatches(inputs) {\n        throw new Error(\'findInputMatches has not been implemented.\');\n    }\n\n    /**\n     * Determines whether one or more values in needles are present in any of\n     * the keys of the haystacks.\n     * Checks both raw input and base64 encoded versions of the needles.\n     * @param needles An object. The haystacks are searched for the values of\n     * this object.\n     * @param haystacks The array of strings in which the needle(s) may reside.\n     * A string may optionally be supplied if only one value is present.\n     * @returns {[String]} The keys of the needles that were found in one or\n     * more haystacks in either raw or base64 encoded form.\n     */\n    static atLeastOneNeedleInHaystack(needles, haystacks) {\n        if (\n            needles === undefined ||\n            needles.length === 0 ||\n            haystacks === undefined\n        ) {\n            return [];\n        }\n\n        const searchArray = Array.isArray(haystacks) ? haystacks : [haystacks];\n\n        return Object.entries(needles)\n            .filter(([_, needle]) =>\n                searchArray.some(haystack => {\n                    if (isBase64Encoded(haystack)) {\n                        if (atob(haystack).indexOf(needle) !== -1) {\n                            return true;\n                        }\n                    }\n                    return haystack.indexOf(needle) !== -1;\n                })\n            )\n            .map(([input, _]) => input);\n    }\n}\n/*\n * Matcher for an object that cannot contain any inputs.\n */\nclass EmptyMatcher extends InputMatcher {\n    findInputMatches(inputs) {\n        return [];\n    }\n}\n\nclass URLParamsMatcher extends InputMatcher {\n    constructor(url) {\n        super();\n        this._url = url;\n    }\n\n    findInputMatches(inputs) {\n        try {\n            const parsedUrl = new URL(this._url);\n            return InputMatcher.atLeastOneNeedleInHaystack(inputs, [\n                ...parsedUrl.searchParams.values(),\n            ]);\n        } catch (e) {\n            return [];\n        }\n    }\n}\n\nclass StringBodyMatcher extends InputMatcher {\n    constructor(str) {\n        super();\n        this._str = str;\n    }\n\n    findInputMatches(inputs) {\n        let decoded;\n        try {\n            decoded = decodeURIComponent(this._str);\n        } catch (e) {\n            decoded = this._str;\n        }\n\n        const searchParams = new URLSearchParams(decoded);\n        const parsedQuery = {};\n        const parsedValues = [];\n\n        for (const [key, value] of searchParams.entries()) {\n            parsedQuery[key] = value;\n            parsedValues.push(value);\n        }\n\n        if (parsedQuery !== {} && parsedValues.indexOf(null) === -1) {\n            return InputMatcher.atLeastOneNeedleInHaystack(\n                inputs,\n                parsedValues\n            );\n        }\n        return InputMatcher.atLeastOneNeedleInHaystack(inputs, decoded);\n    }\n}\n\nclass FormBodyMatcher extends InputMatcher {\n    constructor(formData) {\n        super();\n        this._formData = formData;\n    }\n\n    findInputMatches(inputs) {\n        return InputMatcher.atLeastOneNeedleInHaystack(\n            inputs,\n            Object.values(this._formData).flat()\n        );\n    }\n}\n/**\n * Takes a string and removes a variety of \'special\' characters like punctuation.\n * @param str The string to be modified.\n * @returns {string} The resulting string without any special characters.\n */\nfunction stripSpecialChars(str) {\n    return str.replace(/[`~!@#$%^&*()_|+\\-=?;:\'\",.<>}{\\][\\\\/]/gi, \'\');\n}\n\n/**\n * Attempts to identify where a string is base-64 encoded or not.\n * @param string The raw string to be examined.\n * @returns {boolean} Returns true if the string matches a base-64 string. False otherwise.\n */\nfunction isBase64Encoded(string) {\n    return /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/.test(\n        string\n    );\n}\n\n// Computes the base64 sha512/sha224 hash of some text, removing trailing ==\nfunction base64hash(hashFunction, text) {\n    return btoa(\n        hashFunction\n            .array(text)\n            .map(char => String.fromCharCode(char))\n            .join(\'\')\n    ).slice(0, -2);\n}\n\nfunction normaliseUrl(str) {\n    const link = document.createElement(\'a\');\n    link.href = str;\n    return link.href;\n}\n\nfunction stripCredentials(urlstr) {\n    try {\n        const url = new URL(urlstr);\n        const params = url.searchParams;\n        params.forEach((_, key) => params.set(key, \'\'));\n        return url.toString();\n    } catch (e) {\n        return urlstr;\n    }\n}\n\n/**\n * Check if two URLs match regardless of any partial encoding.\n *\n * Uses decodeURI which can throw a URIError when url is malformed.\n */\nfunction urlsMatch(first, second) {\n    try {\n        return decodeURI(first) === decodeURI(second);\n    } catch (e) {\n        return false;\n    }\n}\n\nfunction sendMessage(type, data, tabId, options) {\n    if (tabId) {\n        chrome.tabs.sendMessage(tabId, { data, type }, options);\n    } else {\n        chrome.runtime.sendMessage({ data, type });\n    }\n}\n\n// Encodes the given input using base64url as defined by RFC4648 section 5.\nfunction base64Url(input) {\n    return btoa(input)\n        .replace(\'+\', \'-\')\n        .replace(\'/\', \'_\');\n}\n/*\n * Netcraft Extension\n * constants JavaScript\n */\n\n/* eslint sort-keys: \'off\' */\n/* sort-keys is disabled as the constants are separated by type */\n\n// Aligned values here make for easier reading, so we disable prettier\n\n// prettier-ignore\nconst cons = (function() {\n    const cryptojackersUrl = \'https://www.netcraft.com/apps/help/glossary/#crypto-miners\';\n    const skimmersUrl = \'https://www.netcraft.com/apps/help/glossary/#credit-card-skimmers\';\n    const evilResourceCrime = \'attempted to load\';\n\n    return {\n        fields: [\n            \'topsites\',\n            \'country\',\n            \'netblock\',\n            \'hoster\',\n            \'firstseen\',\n            \'rank\',\n            \'risk\',\n            \'uses_sslv3\',\n            \'pfs\',\n            \'heartbleed\',\n            \'heartbleed_message\',\n        ],\n\n        // options that can be toggle in options.js and installed flag\n\n        toggleOptions: [\n            \'installed\',\n            \'block\',\n            \'xss\',\n            \'suspicious\',\n            \'other-malicious-scripts\',\n            \'credential-leaks\',\n            \'analytics\',\n            \'cryptojackers\',\n            \'skimmers\',\n        ],\n\n        // domains\n        mirror:  \'https://mirror2.extension.netcraft.com/\',\n        toolbar: \'https://mirror.toolbar.netcraft.com/\',\n\n        // links and faqs\n        blogArchive:      \'https://news.netcraft.com/archives/\',\n        browserApp:       \'https://www.netcraft.com/apps/browser/\',\n        cryptojackersUrl,\n        extensionStore:   \'https://addons.mozilla.org/en-us/firefox/addon/netcraft-toolbar?src=external-blocked_visit-rating\',\n        heartbleedUrl:    \'https://www.netcraft.com/apps/help/glossary/#heartbleed\',\n        logBlockedVisit:  \'https://toolbar.netcraft.com/blocked_visit\',\n        netblock:         \'https://sitereport.netcraft.com/netblock?q=\',\n        pfsUrl:           \'https://www.netcraft.com/apps/help/glossary/#pfs\',\n        report:           \'https://sitereport.netcraft.com/?url=\',\n        reportAPI:        \'https://report.netcraft.com/api/v2/report/urls\',\n        reportMistake:    \'https://report.netcraft.com/report/mistake?url=\',\n        skimmersUrl,\n        sslv3Url:         \'https://www.netcraft.com/apps/help/glossary/#sslv3\',\n        submissionURL:    \'https://report.netcraft.com/submission/\',\n        topsites:         \'https://trends.netcraft.com/topsites?s=\',\n\n        // paths\n        block:              \'blocked.html\',\n        check:              \'check_url/v4/\',\n        danger:             \'Images/danger.png\',\n        embed:              \'/embedded\',\n        evilJSPatternsFeed: \'eviljspatterns\',\n        skimmerDomainWhitelistFeed: \'eviljsskimmerwhitelist\',\n        evilResourcesFeed:  \'blockdb/netcraft_extension_evil_resource_encrypted-yppL7bVh/\',\n        flags:              \'Images/flags/\',\n        force:              \'/dodns\',\n        heartbleed:         \'Images/heartbleed.png\',\n        info:               \'/info\',\n        poodlePost:         \'2014/10/15/googles-poodle-affects-oodles.html\',\n        warning:            \'Images/warning.png\',\n\n        // feed constants\n        evilResourceTypes: [\'cryptojacker\', \'skimmer\'],\n        evilResourcesSalt: \'C4jhYHxW\',\n        maxRevisions:      5,\n        randomSaltLength:  8,\n\n        // mappings of url_type => information\n        urlTypeInformationMappings: {\n            \'malicious_javascript/cryptojacker\': [\'web miners\',             cryptojackersUrl],\n            \'malicious_javascript/skimmer\':      [\'shopping site skimmers\', skimmersUrl],\n        },\n\n        // mappings of evilResourceTypes => user options\n        evilResourceOptionMappings: {\n            \'cryptojacker\': \'cryptojackers\',\n            \'skimmer\':      \'skimmers\'\n        },\n\n        // mappings of user options => url_type\n        optionUrlTypeMappings: {\n            \'block\':                   /^(?!malicious_javascript:)/,\n            \'cryptojackers\':           /^malicious_javascript\\/cryptojacker$/,\n            \'other-malicious-scripts\': /^malicious_javascript\\/(?!(skimmer|cryptojacker)$)/,\n            \'skimmers\':                /^malicious_javascript\\/skimmer$/,\n        },\n\n        // report constants\n        malwareSourceEmail:  \'extension-reported-malware@netcraft.com\',\n        malwareSourceUUID:   \'mwcCcApi89cobY9loJ9rWJ9HuOhlpwMj\',\n        extensionSourceUUID: \'qBEQ3NhEnwK4sypmuSxBJzRqpxZLRXxe\',\n\n        // report error default message\n        reportErrorMessage: \'An error occured with your submission. Please try again.\',\n\n        // mapping from report API errors to displayed messages\n        reportErrorMappings: {\n            \'Duplicate of a recent submission\': \'This URL has already been reported.\',\n            \'Supplied email is not valid\': \'Invalid email address. Please correct it and try again.\',\n        },\n\n        whitelistErrorMessages: {\n            invalidHostname: \'Invalid hostname, please correct it and try again\',\n            duplicate: \'Protection is already disabled for this site\',\n        },\n\n        // browserAction icon paths\n        iconNormal: {\n            16: \'Images/action-normal-16.png\',\n            24: \'Images/action-normal-24.png\',\n            32: \'Images/action-normal-32.png\'\n        },\n        iconDanger: {\n            16: \'Images/action-danger-16.png\',\n            24: \'Images/action-danger-24.png\',\n            32: \'Images/action-danger-32.png\'\n        },\n        iconWarning: {\n            16: \'Images/action-warning-16.png\',\n            24: \'Images/action-warning-24.png\',\n            32: \'Images/action-warning-32.png\'\n        },\n        iconDisabled: {\n            16: \'Images/action-disabled-16.png\',\n            24: \'Images/action-disabled-24.png\',\n            32: \'Images/action-disabled-32.png\'\n        },\n\n        // Extension internal message types\n        capturedIntercept:  \'interceptDetailsCaptured\',\n        confirmedIntercept: \'interceptDetailsConfirmed\',\n        contentReady:       \'contentScriptReady\',\n        leaksEnabled:       \'credentialLeaksEnabled\',\n        reportURL:          \'report\',\n        reqBlock:           \'block\',\n        reqDetails:         \'details\',\n        reqForce:           \'force\',\n        reqPopUp:           \'popup\',\n        reqPrintTimingData: \'time\', //debug\n        reqInputs:          \'requestInputValues\',\n        sendInputs:         \'sendInputs\',\n        sendIntercept:      \'sendInterceptDetails\',\n        updateOption:       \'updateOption\',\n        updateWhitelist:    \'whitelist\', // disable button whitelist\n\n        // Status codes\n        errEvilResource: \'EVILRESOURCE\',\n        errNoDetails:    \'NODETAILS\',\n        errNoInfo:       \'NOINFO\',\n        errBlocked:      \'BLOCKED\',\n        errPending:      \'PENDING\',\n        errServer:       \'ERROR\',\n        errSuspicious:   \'SUSPICIOUS\',\n        errTimeout:      \'TIMEOUT\',\n        errXSS:          \'XSS\',\n        noError:         \'OK\',\n\n        // Valid protocols (it is a regex match so ^ matches start of string)\n        protocols:    [\'^http\', \'^https\', \'^ftp\'],\n\n        // Human readable messages\n        msgNa:           \'NA\',\n        msgNewSite:      \'New Site\',\n        msgNoInfo:       \'Site information not available\',\n        msgNoServer:     \'Unable to contact Netcraft servers\',\n        msgSuspicious:   \'Suspicious URL Detected\',\n        msgXSS:          \'Suspected XSS Attack\',\n        msgBlocked:      {\n            \'malicious_javascript/skimmer\': {\n                title: \'Shopping Site Skimmer Detected\',\n                description: \'shopping site skimmer\'\n            },\n            \'malicious_javascript/dropsite\': {\n                title: \'Potential Credential Leak Detected\',\n                description: \'potential credential leak\'\n            },\n            \'malicious_javascript/cryptojacker\': {\n                title: \'Web Miner Detected\',\n                description: \'web miner\'\n            },\n            \'malware_c2/infrastructure\': {\n                title: \'Malicious URL\',\n                description: \'malicious page\'\n            },\n            \'default\': {\n                title: \'Suspected Phishing\',\n                description: \'suspected phishing site\'\n            },\n        },\n\n        // Human readable reasons\n        urlTypeCrimeMappings: {\n            \'malicious_javascript/skimmer\':      evilResourceCrime,\n            \'malicious_javascript/cryptojacker\': evilResourceCrime,\n            \'malicious_javascript/dropsite\':     \' is blocked because we detected that sensitive information was leaked from\',\n        },\n\n        // Timeout function value to clear cache etc.\n        timeout:      300000,\n\n        // Skimmer whitelist update interval (1 day)\n        skimmerWhitelistInterval: 1000 * 60 * 60 * 24,\n\n        // Google Analytics account\n        account:      \'UA-2150242-8\',\n        gaCategory:   \'Popup load times\',\n\n        // InputAnalyser constants\n        minInputSize: 5,\n\n        // First addresses of the private IPv4 netblocks\n        privateIPv4Blocks: [\n            \'10.0.0.0\',\n            \'172.16.0.0\',\n            \'192.168.0.0\'\n        ],\n\n        // Sync storage key for disable for this site button whitelist\n        disableButtonWhitelistKey: \'disableButtonWhitelist\',\n\n        // Local storage key for local credential leak detection whitelist\n        localCredentialLeakWhitelistKey: \'localCredentialLeakWhitelist\',\n\n        // Whitelisted fileName regexes for credential leak detection\n        credentialLeakFileNameWhitelist: [\n            /^https?:\\/\\/googleads.g.doubleclick.net\\//,\n        ],\n    };\n})();\n\n/*\n * recursively prevents any property or sub-property in cons from:\n *   - being added;\n *   - being removed; or\n *   - being modified.\n */\nfunction deepFreeze(object) {\n    Object.freeze(object);\n\n    for (const name of Object.getOwnPropertyNames(object)) {\n        if (typeof object[name] === \'object\') {\n            deepFreeze(object[name]);\n        }\n    }\n}\n\ndeepFreeze(cons);\n/*\n * RelevantInput objects check whether given form inputs should be\n * treated as sensitive data.\n *\n * This should be treated as an abstract class. Only the subclasses\n * are intended to be instantiated.\n */\nclass RelevantInput {\n    /*\n     * Enum of the possible return values of test():\n     * RELEVANT:\n     *     Input is relevant now, outgoing requests should be\n     *     checked for the contained value (subject to minimum length).\n     * POTENTIALLY RELEVANT:\n     *     Input should not be considered relevant now, but may be in\n     *     future so should continue to be checked.\n     * NOT_RELEVANT:\n     *     Input is not relevant and will not become relevant, so can\n     *     be ommitted from further checks to optimise performance.\n     */\n    static get relevance() {\n        return {\n            NOT_RELEVANT: 1,\n            POTENTIALLY_RELEVANT: 2,\n            RELEVANT: 3,\n        };\n    }\n\n    /*\n     * relevantInputTypes (string) - a comma separated list of\n     * selectors that describe the types of input that should be\n     * considered relevant.\n     */\n    constructor(relevantInputTypes) {\n        this.relevantInputTypes = relevantInputTypes;\n    }\n\n    /*\n     * Checks whether any of the given input\'s \'labels\' (the ID, name,\n     * or actual label) match the given regex.\n     */\n    static testInputLabel(regex, input) {\n        const labels = [\n            input.id,\n            input.name,\n            InputAnalyser.getLabelText(input),\n        ];\n        return labels.some(label => regex.test(label));\n    }\n}\n\n/*\n * RelevantInput for passwords.\n */\nclass Password extends RelevantInput {\n    constructor() {\n        super(\'input[type=password]\');\n    }\n\n    /*\n     * Passwords are verified purely by their input type.\n     */\n    test(input) {\n        if (input.matches(this.relevantInputTypes)) {\n            return RelevantInput.relevance.RELEVANT;\n        } else {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n    }\n}\n\n/*\n * RelevantInput for card numbers.\n */\nclass CardNumber extends RelevantInput {\n    constructor() {\n        // Inputs with no type attribute act like text inputs\n        super(\n            \'input[type=text], input[type=tel], input[type=number], input:not([type])\'\n        );\n\n        // Definitely card number labels\n        this.inputLabelRegexes = [\n            /(cc|card).?(num|no)/i,\n            /&#12459;&#12540;&#12489;&#30058;&#21495;/,\n            /&#1053;&#1086;&#1084;&#1077;&#1088;.*&#1082;&#1072;&#1088;&#1090;&#1099;/i,\n            /&#20449;&#29992;&#21345;&#21345;&#34399;/i,\n            /&#20449;&#29992;&#21345;&#21495;&#30721;/i,\n        ];\n\n        /*\n         * Matches incomplete card numbers, because ideally the\n         * skimmer should be detected before the victim gives away\n         * most of their card number.\n         */\n        this.inputValueRegexes = [/^(\\d{4}[-\\s]?){0,4}\\d{1,4}$/];\n\n        // Probably card number labels\n        this.suspectInputLabelRegexes = [\n            /card/i,\n            /tarjeta/i,\n            /carte/i,\n            /&#1082;&#1072;&#1088;&#1090;&#1099;/i,\n            /&#21345;/i,\n            /&#12459;&#12540;&#12489;/i,\n            /&#52852;&#46300;/i,\n            /credit[^s]/i,\n        ];\n\n        // If it matches one of these, probably not a card number\n        this.inputLabelExceptionRegexes = [\n            /post/i,\n            /code/i,\n            /zip/i,\n            /acc(oun)?t?[-_]?n(um|o)/i,\n            /credit[-_]?(sum|limit)/i,\n            /loyalty/i,\n            /gift/i,\n            /idcard/i,\n            /cardid/i,\n            /club/i,\n            /sku/i,\n            /(\\b|_)otp/i,\n            /price/i,\n            /amount/i,\n            /service/i,\n            /picard/i,\n            /search/i,\n            /contact/i,\n            /callme/i,\n            /chat/i,\n            /activate/i,\n            /msisdn/i,\n        ];\n    }\n\n    test(input) {\n        if (!input.matches(this.relevantInputTypes)) {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n\n        // Return NOT_RELEVANT if label matches an exception regex\n        if (\n            this.inputLabelExceptionRegexes.some(r =>\n                RelevantInput.testInputLabel(r, input)\n            )\n        ) {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n\n        // Return RELEVANT if label clearly states card number\n        if (\n            this.inputLabelRegexes.some(r =>\n                RelevantInput.testInputLabel(r, input)\n            )\n        ) {\n            return RelevantInput.relevance.RELEVANT;\n        }\n\n        /*\n         * If label suggests card number, return RELEVANT if value\n         * also suggests card number or POTENTIALLY_RELEVANT if\n         * not. Otherwise, return NOT_RELEVANT as this element will\n         * not become relevant unless a label changes (unlikely).\n         */\n        if (\n            this.suspectInputLabelRegexes.some(r =>\n                RelevantInput.testInputLabel(r, input)\n            )\n        ) {\n            if (this.inputValueRegexes.some(r => r.test(input.value))) {\n                return RelevantInput.relevance.RELEVANT;\n            } else {\n                return RelevantInput.relevance.POTENTIALLY_RELEVANT;\n            }\n        } else {\n            return RelevantInput.relevance.NOT_RELEVANT;\n        }\n    }\n}\n\n    const inputAnalyser = new InputAnalyser();\n\n    const resourceBlockList = new Set([\n                            \"6C590F97DDB1A11AE795B3050E4C3866\", \"D285BE79786E2ECE396941331C5BA65E\", \"59DF8523520DE7F08FB53D3D12BD94F5\", \"1FB784EBAFDA1618023756BF5B6D7E46\", \"0D6E1B5F03B81F5C37813B22E29C4FE2\", \"EFAB0ECE9383D50C555B8168C3E2A4DD\", \"AC8A4B13A411A4E43E61D0A034FA4A55\", \"25A781F6D1F94EEACCF1E00E844D9E14\", \"3920B04ED0D215EA738F524F60485F23\", \"05F4A2D3161A92EB82A44F15FC2A3AEC\", \"C5A1BE7B3855B0A25275B2877E005CB9\", \"00069CEC347076721EBE2257978A0A33\", \"9A513130D02F360BBDC16D8AC0C579D8\", \"7CDB2A3A29E50F90C1000252EF523AD6\", \"25CE912FAF2376026AAD0174AD5A0329\", \"739E1F67AA5F64A06FC469F6030802D4\", \"F49368C9A05C29EF49411999C5987F2C\", \"AB2DD89FB892155275D8780CF527BC93\", \"CFE758EB18FBFE70B1F690241B75D2B1\", \"E43C3463B40A790545F4149FF06B419E\", \"8B6D4C76A7CCF5EB720660B8B2D3F5EB\", \"45573B3E2F613AF5BF2145DF004BABD1\", \"E3DB4E968B371DE94CE607199A062F39\", \"A4EFF1240C5DE614B394FB738CBD28E2\", \"F42D10E84B9A69A8A24062319F425A83\", \"90646BE39982594A58EDF5D2630CCFFD\", \"669853BEA9B84EA51D7302A7FBFF5BC3\", \"0BA566B4777526F451108100107DC5ED\", \"AB19E12BFFBC47244BDCC2403EAA9037\", \"61DAF8537240639277FD1A59