18

Let's say I am working with the following web page:

<html>
<body>
<span id="click">click me</span>
<script>
var hello = function() {
    alert('hello');
}

document.getElementById('click').addEventListener('click', function(e) {
    hello();
});
</script>
</body>
</html>

and my Greasemonkey script is:

// ==UserScript==
// @name        My Script
// @include     http://example.com/hello.html
// @version     1
// @grant       none
// ==/UserScript==

window.hello = function() {
    alert('goodbye');
}

With the Greasemonkey script disabled, clicking the #click element on the page displays the 'hello' alert. With the script enabled, clicking the element displays the 'goodbye' alert.

Simple enough. The hello function from the web page is being replaced by the function in the Greasemonkey script.

Now let's say I want to use a Greasemonkey API. When I set the @grant value to a valid value other than 'none' (e.g. // @grant GM_setClipboard) [which causes Greasemonkey to run the script as a "content script", rather than in the page's scope like with 'none'], the Greasemonkey script fails to work.

window.hello is no longer targeting the correct object on the page.

Replacing window.hello with unsafeWindow.hello looks like it would work, but instead, the following error is thrown in the JS console:

Error: Permission denied to access object

How can I rewrite the Greasemonkey script while having @grant GM_setClipboard set to target and replace the original hello function on the page?

System information:

  • Windows 7 64-bit
  • Firefox 32.0
  • Greasemonkey 2.2
Brock Adams
  • 90,639
  • 22
  • 233
  • 295
iglvzx
  • 498
  • 1
  • 8
  • 22

2 Answers2

32

When you set a @grant value other than none, Greasemonkey activates its sandbox and Greasemonkey 2.0 radically changed unsafeWindow handling.

Now, in order to create or overwrite variables in the target-page scope, you must correctly chose from a menu of techniques. EG:

To Read:

  • A simple variable:

    Target page sets:       var foo = "bar";
    GM script can read:     unsafeWindow.foo    //-- "bar"
    
  • A simple object:

    Target page sets:       var obj = {A: 1};
    GM script can read:     unsafeWindow.obj    //-- Object { A: 1 }
    
  • A complex object: This is not always possible.

To Call:

  • A simple function:

    Target page sets:       function func () {console.log ('Hi');}
    GM script can call:     unsafeWindow.func() //-- "Hi"
    
  • A complex function: This is not always possible.

To Write/Set:

  • A simple variable:

    unsafeWindow.foo = "Apple";
    
  • A simple object:

    var gmObject        = {X: "123"};
    unsafeWindow.obj    = cloneInto (gmObject, unsafeWindow);
    
  • A simple function:

    function gmFunc () {
        console.log ("Lorem ipsum");
        //-- Can use GM_ functions in here! :)
    }
    unsafeWindow.func   = exportFunction (gmFunc, unsafeWindow);
    

Consider this HTML:

<button id="helloBtn">Say "Hello".</button>

And this javascript:

var simpleGlobalVar = "A simple, global var in the page scope.";
var globalObject    = {Letter: "A", Number: 2};

function simpleFunction () {
    console.log ("The target page's simpleFunction was called.");
}

var sayHello = function() {
    console.log ('Hello.');
}

document.getElementById ('helloBtn').addEventListener ('click', function () {
    sayHello ();
} );

which you can see live at this jsFiddle page.

If you install and run this Greasemonkey script against that page:

// ==UserScript==
// @name     _Demonstrate accessing target-page variables with @grant values set
// @include  http://fiddle.jshell.net/sepwL7n6/*/show/
// @require  http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js
// @grant    GM_addStyle
// ==/UserScript==

console.log ("*** Greasemonkey script start.");

$("body").append ('<div id="gmArea">Added by Greasemonkey:<p></p></div>');
$("#gmArea > p:first").append ('<button id="gmShow">Access select target-page variables and functions</button>');
$("#gmArea > p:first").append ('<button id="gmChange">Change javascript things in the target-page scope.</button>');

$("#gmShow").click ( function () {
    //-- Access things from the target-page scope:
    console.log ("----------------");
    console.log ("==> simpleGlobalVar is: ", unsafeWindow.simpleGlobalVar);
    console.log ("==> globalObject    is: ", unsafeWindow.globalObject);
    console.log ("==> Calling target's simpleFunction():");
    unsafeWindow.simpleFunction ();

    //-- WARNING! This next technique is not robust, but works in some cases.
    console.log ("==> Calling target's button's click().");
    unsafeWindow.document.getElementById ('helloBtn').click ();
} );

$("#gmChange").click ( function () {
    this.disabled = true;   //-- Can only click once.
    unsafeWindow.simpleGlobalVar    = "Simple var... Intercepted by GM!";
    unsafeWindow.globalObject       = cloneInto (gmObject, unsafeWindow);
    unsafeWindow.sayHello           = exportFunction (sayHello, unsafeWindow);
    console.log ("==> Target page objects were changed.");
} );

var gmMessageStr    = "Function... Intercepted by GM, but also can use GM_ functions!";
function sayHello () {
    sayHello.K      = (sayHello.K  ||  0) + 1;
    console.log (gmMessageStr);
    GM_addStyle ('body {background: ' + (sayHello.K % 2  ?  "lime"  :  "white") + ';}');
}
var gmObject        = {message: "Object overridden by GM."};


Open the console and press the buttons and you will see that the GM script is able to read and change the page's variables and functions.


Notes:

  1. This is all Firefox specific.
  2. For cross platform code, and for some complex situations, you can use Script Injection instead. But injected code cannot directly access GM_ functions.
  3. Note that these techniques only work for javascript variables and functions that are global.
Community
  • 1
  • 1
Brock Adams
  • 90,639
  • 22
  • 233
  • 295
  • Isn't the new `unsafeWindow` behavior actually from Firefox 30, like it says in [this blog entry](https://blog.mozilla.org/addons/2014/04/10/changes-to-unsafewindow-for-the-add-on-sdk/)? Presumably, the related changes in GM 2.0 had to do with making sure the new APIs were available to GM scripts, and perhaps that GM itself used them properly instead of relying on the old behavior. – SamB Jan 03 '15 at 19:34
  • @SamB, not exactly. Note that that blog entry is about the SDK, not Firefox. GM does not use the SDK and did not have to make the changes it did at that time. It's unclear if planned future changes to FF would eventually force the change, but the new way is better in that it allows easier integration of GM functions into target-page code (a dangerous practice). – Brock Adams Jan 03 '15 at 19:42
3

Wrap your code inside a lambda function like:

(function(window){      // and more arguments if you need it

  console.log(window);  // here, should be the real 'window'

})(window.unsafeWindow)
K_AEI
  • 51
  • 3