torn-pda icon indicating copy to clipboard operation
torn-pda copied to clipboard

'eval' in userscript not working

Open mirrorsysu opened this issue 3 years ago • 9 comments
trafficstars

Hi, I found that the 'eval' function in userscript only works on crimes.php, is that a possible way to use it on all pages?

mirrorsysu avatar May 07 '22 12:05 mirrorsysu

Hi. I've tested and it's working for me everywhere.

Something simple such us:

console.log(eval(1+2));

Torn PDA should not be blocking any of the js functions, unless Torn is blocking it for some reason (but that is not happening to me). What is it that you are trying to accomplish? You can PM me in Discord if you'd need me to test something in particular.

Manuito83 avatar May 08 '22 15:05 Manuito83

Thanks for your reply! After further testing, it seems that Torn had disabled some eval features, which makes me a lot of trouble because I'm building a userscript that works like a mod manager helping users install or update various userscripts. The script is working well in GM now😂. Is it possible for PDA to add evaluateJavascript in a future version? Just like PDA_httpGet.

mirrorsysu avatar May 09 '22 02:05 mirrorsysu

I'm not really sure. What we do with PDA_httpGet is to delegate the API call to the Dart side of the app and then relay the response to the relevant script. But in this case, the app is already performing an evaluateJavascript call for every script that it finds, so I guess it will have the same issue with Torn's restrictions. This, or I'm not understanding something.

The difference with GM is that the later runs in a frame, so you don't have any restrictions on what Torn allows you to do. With mobile platforms you can't do that (except for Chromium apps, I believe).

In general... if you find a way to make your script work in Chrome's console (in desktop, it doesn't really matter) it should run in Torn PDA without much issues. There might be differences from Android and iOS (iOS does not always trigger the onLoadStop method, so you have to do it manually, but that's it).

Could you elaborate a bit more what you propose?

Manuito83 avatar May 09 '22 10:05 Manuito83

Hi, I have tried this code in the address bar in safari javascript:eval(`alert(1)`); I found that this code only works at certain pages, such as 'crime.php' or 'jail.php', and in other pages the console print: image

And when I use a test script in PDA like:

// ==UserScript==
// @match        https://www.torn.com/*
// ==/UserScript==
eval(`alert(1)`);

The alert only shows at 'crime.php' or 'jail.php', which matches the previous result we tested in safari.

So in my opinion, the reason why the userscript doesn't work is because the web browser restricts the eval features.

Maybe there is a possible way to evaluate js code using webView.evaluateJavascrip instead of eval could help us bypassing the restriction : In userscripts/TornPDA_API.js

async function PDA_evaluateJavascript(source) {
    await __PDA_platformReadyPromise;
    return window.flutter_inappwebview.callHandler("PDA_evaluateJavascript", source);
}

In lib/widgets/webviews/webview_full.dart (I'm not good at dart, hope that can work)

void _addScriptApiHandlers(InAppWebViewController webView) {
    ......
    webView.addJavaScriptHandler(
          handlerName: 'PDA_evaluateJavascript',
          callback: (args) async {
              webView.evaluateJavascript(source: args[0]);
              return;
          },
    );
    ......
}

then I can use PDA_evaluateJavascript('alert(1);') to evaluate js code.

I am not a native English speaker and I am sorry if my words are confusing to you, thanks for your quickly reply again.

mirrorsysu avatar May 09 '22 14:05 mirrorsysu

Thanks for the clarification.

So I tested internally with your suggestion (with a quick handler) and it doesn't work either. I guess that Torn has unsafe-eval blocked as part of the Content-Security-Policy, at least in most pages (not sure why it works in Crimes or Jail).

Can I get more details of what you are trying to do with eval? Why can you not execute the code directly from a function? I'd need to see a complete example (even if it's basic) of how you intend to use it, so I can see what/how I can help. I see no difference from using evaluateJavascript in Dart from executing the code directly in JS.

With the API implementation it was different, as Torn does not allow to call another API. But with eval() it seems that it makes no difference. But there might be something we can do if I can understand the goal better.

Again, feel free to PM me. Thanks!

Manuito83 avatar May 09 '22 18:05 Manuito83

My script(named 'ExtCenter') acts as a script manager for this repo. It looks like this: image

ExtCenter will list the scripts in the repo. After the user clicks the install button, ExtCenter will download the script's source code(see $(".extcenter-install").click and corsGet), then try to load the source code in loadScript. In loadScript, eval is used to execute the source code.

Here's a simple demo:

// ==UserScript==
// @name         ExtCenterDemo
// @version      0.0.1
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      *
// ==/UserScript==

(function() {
    'use strict';

    // avoid over loading in pda
    try {
        const __win = window.unsafeWindow || window;
        if (__win.extTest) return;
        __win.extTest = true;
        window = __win; // fix unsafeWindow
    } catch (err) {
        console.log(err);
    }

    const $ = window.jQuery;

    const UserScriptEngineEnums = Object.freeze({
        GM: 'gm',
        PDA: 'pda',
        OTHER: 'other'
    });

    let userScriptEngine = UserScriptEngineEnums.OTHER;
    try {
        GM_xmlhttpRequest;
        userScriptEngine = UserScriptEngineEnums.GM;
    } catch {
        try {
            PDA_httpGet;
            userScriptEngine = UserScriptEngineEnums.PDA;
        } catch {}
    }

    const color_pool = Object.freeze({
        'gray': '#adadad',
        'red': '#ff7373',
        'green': '#8fbc8f',
        'blue': '#65a5d1',
        'purple': '#8d6dd7',
        'yellow': '#f39826',
        'yellowgreen': '#83a000',
        'pink': '#e467b3',
        'salmon': '#F9CDAD',
        'orange': '#FFDEAD'
    });

    function getLocalStorageRootNode(key1) {
        if (window.localStorage === undefined) {
            return undefined;
        } else if (window.localStorage.getItem(key1) === null) {
            return null;
        } else {
            const json = JSON.parse(window.localStorage.getItem(key1));
            return json;
        }
    }

    function getLocalStorage(key1, key2) {
        const json = getLocalStorageRootNode(key1);
        if (json === undefined) {
            return undefined;
        } else if (json === null) {
            return null;
        } else {
            if (json[key2] === undefined) {
                return undefined;
            } else {
                return json[key2];
            }
        }
    }

    function updateLocalStorage(key1, key2, value) {
        if (window.localStorage === undefined) {
            return undefined;
        } else if (window.localStorage.getItem(key1) === null) {
            let json = {};
            json[key2] = value;
            window.localStorage.setItem(key1, JSON.stringify(json));
        } else {
            let json = JSON.parse(window.localStorage.getItem(key1));
            json[key2] = value;
            window.localStorage.setItem(key1, JSON.stringify(json));
        }
    }

    function deleteLocalStorage(key1, key2) {
        if (window.localStorage === undefined) {
            return undefined;
        } else if (window.localStorage.getItem(key1) === null) {
            return null;
        } else {
            const json = JSON.parse(window.localStorage.getItem(key1));
            if (json[key2] === undefined) {
                return undefined;
            } else {
                delete json[key2];
                window.localStorage.setItem(key1, JSON.stringify(json));
            }
        }
    }

    function deleteLocalStorageRootNode(key1) {
        if (window.localStorage === undefined) {
            return undefined;
        } else if (window.localStorage.getItem(key1) === null) {
            return null;
        } else {
            window.localStorage.removeItem(key1);
        }
    }

    function loadScript(name, version, source) {
        console.log(`try to eval ${name} - ${version}`);
        let extIdentifier = `extcenter_${name}`;
        if (window[extIdentifier]) {
            return false;
        } else {
            eval(source);
            window[extIdentifier] = true;
            return true;
        }
    }

    async function corsGet(url) {
        console.log(`[cors] get ${url}`);
        switch (userScriptEngine) {
            case UserScriptEngineEnums.GM:
                return new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        method: 'get',
                        url: url,
                        ontimeout: res => reject(`timeout: ${res}`),
                        onload: res => resolve(res.response),
                        onerror: res => reject(`error: ${res}`),
                    });
                });
            case UserScriptEngineEnums.PDA:
                return new Promise((resolve, reject) => {
                    PDA_httpGet(`${url}`).then((res) => {
                        resolve(res.responseText);
                    }).catch((err) => {
                        reject(`error: ${err}`);
                    });
                });
            case UserScriptEngineEnums.OTHER:
            default:
                return new Promise((_, reject) => {
                    reject(`error:not support cors`);
                });
        }
    }

    (function() {
            function navigatorExtCenter() {
                const extcenter_container = `
            <div style="width: inherit">
                <div style="margin:10px 0px; border:1px solid darkgray; text-align:center;">
                    <button id="extcenter-update" class="torn-btn" style="margin:5px;">Update</button>
                    <button id="extcenter-clear" class="torn-btn" style="margin:5px;">Clear</button>
                    <a href="https://github.com/mirrorsysu/BWExtensions"><button id="extcenter-submit-code" class="torn-btn" style="margin:5px; float:right;">Repo</button></a>
                </div>
                <div style="height:400px; margin:10px 0px; border:1px solid darkgray; text-align:center; overflow:hidden;">
                    <div style="height:300px; overflow:auto;">
                        <table id="extcenter-table" style="width:95%; margin: 5px auto; background-color: white; text-align: center;"></table>
                    </div>
                    <div id="extcenter-status" style="border-top:1px solid darkgray; text-align:center; margin:5px; padding:5px; max-height:100px; overflow:auto"></div>
                </div>
            </div>`;
                return extcenter_container;
            }
            $("#mainContainer").prepend(navigatorExtCenter());

            function updateStatus(text) {
                console.log(text);
                $("#extcenter-status").prepend(`<span style="display:block; margin:2px">${text}</span>`);
            }

            function updateError(err) {
                console.log(err);
                $("#extcenter-status").prepend(`<span style="display:block; color:red; margin:2px">${err}</span>`);
            }

            function updateSpecTable() {
                function findSpecByName(specs, name) {
                    for (let i = 0; i < specs.length; i++) {
                        if (name === specs[i].name) return i;
                    }
                    return -1;
                }

                let installedSpecs = getLocalStorage("extcenter", "installedSpecs") || [];
                let specs = getLocalStorage("extcenter", "specs") ?? [];
                let html = ``;
                installedSpecs.forEach((item) => {
                            let i = findSpecByName(specs, item.name);
                            if (i >= 0 && item.version !== specs[i].version) {
                                html += `<tr><td class="extcenter-name">${item.name}</td>
                    <td>${item.author}</td><td>${item.version}(<span style="color:red">${specs[i].version}</span>)</td>
                    <td${item.detail ?` title="${item.detail}"` :""}>${item.description}</td>
                    <td><button class="extcenter-update-source" spec="${item.name}" style="margin:5px;">Update</button></td>
                    </tr>`;
                    return; // has new version
                }
                html += `<tr><td class="extcenter-name">${item.name}</td>
                <td>${item.author}</td><td>${item.version}</td>
                <td${item.detail ?` title="${item.detail}"` :""}>${item.description}</td>
                <td><button class="extcenter-uninstall" spec="${item.name}" style="margin:5px;">Uninstall</button></td>
                </tr>`;
            });
            specs.forEach((item) => {
                let i = findSpecByName(installedSpecs, item.name);
                if (i >= 0) {
                    return; // has installed
                }
                html += `<tr><td class="extcenter-name">${item.name}</td>
                <td>${item.author}</td><td>${item.version}</td>
                <td${item.detail ?` title="${item.detail}"` :""}>${item.description}</td>
                <td><button class="extcenter-install" spec="${item.name}" style="margin:5px;">Install</button></td>
                </tr>`;
            });

            $("#extcenter-table").html(html);
            $("#extcenter-table td").attr("style", "border: 1px solid darkgray;padding: 5px");
            $(".extcenter-install").attr("style", `background-color: ${color_pool.blue}; color: white; border-radius:5px; cursor:pointer; padding:3px;`);
            $(".extcenter-update-source").attr("style", `background-color: ${color_pool.green}; color: white; border-radius:5px; cursor:pointer; padding:3px;`);
            $(".extcenter-uninstall").attr("style", `background-color: ${color_pool.orange}; color: #002152; border-radius:5px; cursor:pointer; padding:3px;`);
            $("#extcenter-table .extcenter-name").css("font-weight", "bold");
            $("#extcenter-table .extcenter-desc").css("min-width", "200px");

            $(".extcenter-install").click(async function() {
                try {
                    const spec = specs[findSpecByName(specs, $(this).attr("spec"))];
                    const URL = `https://raw.githubusercontent.com/mirrorsysu/BWExtensions/master/extensions/${spec.file}?${new Date().getTime()}`;
                    updateStatus(`${spec.name} - ${spec.version} start downloading`);
                    let source = await corsGet(URL);
                    console.log(source);
                    updateStatus(`${spec.name} - ${spec.version} downloaded`);
                    if (userScriptEngine == UserScriptEngineEnums.PDA) {
                        await navigator.clipboard.writeText(source);
                        updateStatus(`${spec.name} - ${spec.version} source has been copied to clipboard, please add into PDA manually.`);
                        return;
                    }
                    if (spec.match && !window.location.href.match(new RegExp(spec.match))) {
                        updateStatus(`${spec.name} - ${spec.version} will load when enter pecifying page next time`);
                    } else {
                        let loaded = loadScript(spec.name, spec.version, source);
                        if (loaded) {
                            updateStatus(`${spec.name} - ${spec.version} load success`);
                        } else {
                            updateStatus(`${spec.name} - ${spec.version} has been loaded`);
                        }
                    }
                    updateLocalStorage("extcenter-source", spec.name, source);
                    installedSpecs.push(spec);
                    updateLocalStorage("extcenter", "installedSpecs", installedSpecs);
                    updateSpecTable();
                } catch (err) {
                    updateError(err);
                }
            });

            $(".extcenter-update-source").click(async function() {
                try {
                    const installedIdx = findSpecByName(installedSpecs, $(this).attr("spec"));
                    const installedSpec = installedSpecs[installedIdx];
                    const spec = specs[findSpecByName(specs, $(this).attr("spec"))];
                    const URL = `https://raw.githubusercontent.com/mirrorsysu/BWExtensions/master/extensions/${spec.file}?${new Date().getTime()}`;
                    updateStatus(`${spec.name} - ${spec.version} start updating`);
                    let source = await corsGet(URL);
                    console.log(source);
                    updateStatus(`${spec.name} - ${installedSpec.version} => ${spec.version} update done, will load after refresh`);
                    updateLocalStorage("extcenter-source", spec.name, source);
                    installedSpecs[installedIdx] = spec;
                    updateLocalStorage("extcenter", "installedSpecs", installedSpecs);
                    updateSpecTable();
                } catch (err) {
                    updateError(err);
                }
            });

            $(".extcenter-uninstall").click(async function() {
                try {
                    const installedIdx = findSpecByName(installedSpecs, $(this).attr("spec"));
                    const spec = installedSpecs[installedIdx];
                    installedSpecs.splice(installedIdx, 1);
                    deleteLocalStorage("extcenter-source", spec.name);
                    updateLocalStorage("extcenter", "installedSpecs", installedSpecs);
                    updateSpecTable();
                    updateStatus(`${spec.name} - ${spec.version} has been deleted`);
                } catch (err) {
                    updateError(err);
                }
            });
        }

        updateSpecTable();

        $("#extcenter-update").click(function() {
            updateStatus("start updating repo");
            const specsURL = `https://raw.githubusercontent.com/mirrorsysu/BWExtensions/master/specs_en.json?${new Date().getTime()}`;
            corsGet(specsURL).then((res) => {
                console.log(res);
                updateStatus("update repo done");
                try {
                    updateLocalStorage("extcenter", "specs", JSON.parse(res));
                    updateSpecTable();
                } catch (err) {
                    updateError(err);
                }
            }).catch(err => {
                updateError(err);
            });
        });

        $("#extcenter-clear").click(function() {
            updateStatus("start cleaning cache");
            deleteLocalStorageRootNode("extcenter");
            updateStatus("extcenter cleaned");
            deleteLocalStorageRootNode("extcenter-source");
            updateStatus("extcenter-source cleaned");
            updateSpecTable();
        });
    })();


    // extcenter
    function loadInstalledSpecs() {
        const installedSpecs = getLocalStorage("extcenter", "installedSpecs") || [];
        installedSpecs.forEach((spec) => {
            try {
                if (spec.match && !window.location.href.match(new RegExp(spec.match))) {
                    console.log(`${spec.name} - ${spec.version} not in specify page`);
                } else {
                    const source = getLocalStorage("extcenter-source", spec.name);
                    let loaded = loadScript(spec.name, spec.version, source);
                    if (loaded) {
                        console.log(`${spec.name} - ${spec.version} load success`);
                    } else {
                        console.log(`${spec.name} - ${spec.version} has been loaded`);
                    }
                }
            } catch (err) {
                console.log(err);
            }
        });
    }
    loadInstalledSpecs();

})();

mirrorsysu avatar May 10 '22 06:05 mirrorsysu

Oh I get it now, thanks a lot.

So, what you are saying is that by implementing a handler, you won't even need to use eval, but just the handler, correct?

Is it possible for you to join the beta (in Discord) so that we can test it?

Manuito83 avatar May 10 '22 18:05 Manuito83

@mirrorsysu alternatively have a look here in case you are not able to join the beta: bb2f01843ad5855ed66621cd8ab54053eb3904c6

Manuito83 avatar May 11 '22 06:05 Manuito83

Thanks for your work! Unfortunately, discord is blocked in my country so I cant get access to it, but https://github.com/Manuito83/torn-pda/commit/bb2f01843ad5855ed66621cd8ab54053eb3904c6 is what I need, thanks~

mirrorsysu avatar May 12 '22 13:05 mirrorsysu

Solved

Manuito83 avatar Feb 09 '24 17:02 Manuito83