support icon indicating copy to clipboard operation
support copied to clipboard

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

Open cyfung1031 opened this issue 1 year ago • 6 comments

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)

  • string property version - indicate the installed version of FireMonkey.
  • function property isInstalled - accepts two string parameters (name and namespace) and return a Promise.
    • return the version code if it is installed; return null if it is not installed.

cyfung1031 avatar Jul 30 '23 03:07 cyfung1031

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.

erosman avatar Jul 30 '23 06:07 erosman

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.

cyfung1031 avatar Jul 30 '23 15:07 cyfung1031

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.

erosman avatar Jul 30 '23 15:07 erosman

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 ?

cyfung1031 avatar Jul 30 '23 16:07 cyfung1031

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:

  • Window: external property is deprecated
  • There can be privacy/security concerns
  • Support is not unified
  • It appears to be an arrangement between GreasyFork (and a few others) and TM|VM|Stylus

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

  • window.external is not supported

Tampermonkey

  • Added in v4.2 (2016-11-24) as experimental
  • Available on tampermonkey.net, greasyfork.org, sleazyfork.org, openuserjs.org, userstyles.org, userscript.zone via optional settings image
  • Available on normal tabs, but not in containers or private window
  • namespace is optional
  • Methods returns a callback
  • Return value is an object
  • See also: Add an option to allow communication with cooperate pages
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

erosman avatar Jul 31 '23 13:07 erosman

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

gunir avatar Sep 22 '23 07:09 gunir