Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to access `window` (Target page) objects when @grant values are set?

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
like image 354
iglvzx Avatar asked Sep 11 '14 03:09

iglvzx


2 Answers

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.
like image 181
Brock Adams Avatar answered Oct 20 '22 19:10

Brock Adams


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)
like image 35
K_AEI Avatar answered Oct 20 '22 20:10

K_AEI