erosman / support

Support Location for all my extensions
Mozilla Public License 2.0
174 stars 12 forks source link

[FireMonkey] Please add Firemonkey to `window.external` #582

Open cyfung1031 opened 1 year ago

cyfung1031 commented 1 year ago

according to this, userscript pages rely on the window.external.xxxx to check whether the script is installed or not.

This is already well implemented in Tampermonkey and Violentmonkey, FireMonkey shall follow this practice and ask those pages like "Greasy Fork" to update accordingly.

Without window.external.xxxx, no one can detect the script is installed or not in FireMonkey.

Suggested Usage

window.external.FireMonkey (or Firemonkey)

erosman commented 1 year ago

according to this, userscript pages rely on the window.external.xxxx to check whether the script is installed or not.

Window: external property is deprecated. Furthermore, window object can be accessible to the webpage which then can interfere with the userscript.

If you mean that the userscript can check which manager it is running on, then the proper method would be GM_info.scriptHandler and GM.info.scriptHandler.

Manager version is also available under GM.info.version. Script name and other details are available under GM.info.script.

cyfung1031 commented 1 year ago

So how can we know other scripts are installed or not?

For example, I can run in the devTools to check whether a script "MyUserscript" is installed or not.


window.external.Violentmonkey.isInstalled('MyUserscript', 'namespace.com').then(result=>{

if(result) console.log(`MyUserscript is installed, version = ${result}`);
else console.log(`MyUserscript is not installed.`);

});

Even I create a UserScript installed in Firemonkey, I can just have GM.info refer to the current script. It is still unable to obtain the installation status of a particular userscript.

erosman commented 1 year ago

So how can we know other scripts are installed or not?

Userscript managers show the active userscripts in their UI. Each userscript manager should have details of its own installation, not another extension's installation.

window in GM|TM|VM in content context is not the same window as userScript context used by FireMonkey. Even if such a variable is added in FM, it wont be accessible to GM|TM|VM.

content context window object is accessible to the webpage. Webpages should not be allowed to check if user has installed other extensions or userscripts. Furthermore, a userscript shall not be allowed to check if user has installed other extensions or userscripts.

I am still unsure of what are you trying to achieve.

cyfung1031 commented 1 year ago

That means it is never possible to let GreasyFork to detect the script is installed or not? Even through there is a userscript written by someone to do this intentionally ?

erosman commented 1 year ago

That means it is never possible to let GreasyFork to detect the script is installed or not?

ATM, webpages won't be able to check if a userscript is installed in FireMonkey. Why should it be allowed?

Preamble

Please note the following:

GreasyFork

GreasyFork only checks TM|VM & Stylus

https://github.com/JasonBarnabe/greasyfork/blob/e198546a9c8e61556aee756932abf222506f4541/app/javascript/managers.js#L1-L21

export function getTampermonkey() {
  return window.external?.Tampermonkey
}

export function getViolentmonkey() {
  return window.external?.Violentmonkey
}

export function canInstallUserJS() {
  return getTampermonkey() || getViolentmonkey()
}

export async function canInstallUserCSS() {
  if (localStorage.getItem('stylusDetected') == '1') {
    return true;
  }
  postMessage({ type: 'style-version-query', name: "whatever", namespace: "whatever", url: location.href }, location.origin);
  return new Promise(resolve => setTimeout(function() {
    resolve(localStorage.getItem('stylusDetected') == '1')
  }, 200));
}

https://github.com/JasonBarnabe/greasyfork/blob/f7dae27d036f02753c17644bb26a0f1b8366bf22/app/javascript/versioncheck.js#L3-L25

function getInstalledVersion(name, namespace) {
  return new Promise(function(resolve, reject) {
    let tm = getTampermonkey()
    if (tm) {
      tm.isInstalled(name, namespace, function (i) {
        if (i.installed) {
          resolve(i.version);
        } else {
          resolve(null);
        }
      });
      return;
    }

    let vm = getViolentmonkey();
    if (vm) {
      vm.isInstalled(name, namespace).then(resolve);
      return;
    };

    reject()
  });
}

Greasemonkey

Tampermonkey

window.external.Tampermonkey = {
  getVersion() { },
  openOptions() { },
  isInstalled() { }
}

Examples

!!window.external?.Tampermonkey
// true

window.external.Tampermonkey.getVersion(console.log)
// Object { version: "4.19.0", id: "fire" }

window.external.Tampermonkey.isInstalled('W.A.R. Links Checker Premium', 'premium version', console.log)
window.external.Tampermonkey.isInstalled('W.A.R. Links Checker Premium', console.log)
// Object { name: "W.A.R. Links Checker Premium", installed: true, enabled: true, version: "1.5.7.8" }

window.external.Tampermonkey.isInstalled('xyz', console.log)
// Object { name: "xyz", installed: false, enabled: undefined, version: undefined }

window.external.Tampermonkey.openOptions()
// open options page, allow web site to navigate user to the TM options page

// background.js
else if("openOptions"==e.method){let r=e.params||"";r&&!r.startsWith("#")&&(r="#"+r),q.tabs.update(t.tab.id,{url:q.extension.getURL("options.html")+r,active:!0},()=>n({}))

window.external.Tampermonkey.openOptions('hash')
// open options page and navigate to the hash anchor, allow web site to navigate user to the TM options page

Violentmonkey

window.external.Violentmonkey = {
  version: '2.15.0',
  isInstalled() { }
}

Examples

!!window.external?.Violentmonkey
// true

window.external.Violentmonkey.version
// "2.15.0"

window.external.Violentmonkey.isInstalled('W.A.R. Links Checker Premium', 'premium version').then(console.log)
// "1.6.2.2"

window.external.Violentmonkey.isInstalled('W.A.R. Links Checker Premium').then(console.log)
window.external.Violentmonkey.isInstalled('xyz', 'premium').then(console.log)
// null

Privacy/Security

Wherever the object is available:

🔶 🔷 🔶 🔷 🔶 🔷 🔶 🔷 🔶 🔷 🔶

window.external from the UserScript

However, a userscript can announce itself by creating the object. Here is an example:

Synchronous Example

// ==UserScript==
// @name         window.external #582
// @description  Please add FireMonkey to window.external #582
// @match        https://greasyfork.org/*
// @match        https://sleazyfork.org/*
// @version      1.0
// ==/UserScript==

const js = `window.external.FireMonkey = {
  version: ${JSON.stringify(GM.info.version)},
  isInstalled(name, namespace) {
    // only for self, return script version
    return name === ${JSON.stringify(GM.info.script.name)} ? ${JSON.stringify(GM.info.script.version)} : null;
  }
};`;

GM.addElement('script', {textContent: js});
window.external.FireMonkey.version
// "2.73"

window.external.FireMonkey.isInstalled('window.external #582')
// "1.0"

Asynchronous Example

// ==UserScript==
// @name         window.external #582
// @description  Please add FireMonkey to window.external #582
// @match        https://greasyfork.org/*
// @match        https://sleazyfork.org/*
// @version      1.0
// ==/UserScript==

const js = `window.external.FireMonkey = {
  version: ${JSON.stringify(GM.info.version)},
  async isInstalled(name, namespace) {
    // only for self, return script version
    return name === ${JSON.stringify(GM.info.script.name)} ? ${JSON.stringify(GM.info.script.version)} : null;
  }
};`;

GM.addElement('script', {textContent: js});
window.external.FireMonkey.isInstalled('window.external #582').then(console.log)
// 1.0
gunir commented 1 year ago

Honestly this feature looks dangerous, and it's not that useful rather not relying on it and have a better coding practice.