9

I am writing a userscript that is injected into a webpage. The script reads some data from a web-server and I want to send messages to a listening application to react to the data.

For now, all I'm doing is trying to send a string command to my listening application and see if I can read it. My code worked before it was injected, but afterwards I get an "undefined reference error".

I suspect that this has something to do with this "Greasemonkey access violation". However, I have been unable to find a solution that works. I'm developing in Chrome.

Here is the section of code I can't get to work.

GM_xmlhttpRequest({
   method: "POST", 
   url: "http://localhost:7777", 
   data: "testing123",
   headers:  {
         "Content-Type": "application/x-www-form-urlencoded"
             },
   onload: function(response) 
   {
      if (response.responseText.indexOf("TEST") > -1) 
      {
         console.log("Response confirmed..."); 
      }
   }
}); 

I'm pretty new to scripting so maybe I'm missing something obvious. How do I get this to work in injected code?

Brock Adams
  • 90,639
  • 22
  • 233
  • 295
akagixxer
  • 1,767
  • 3
  • 21
  • 35
  • Chrome version? Can you show your meta-data block (`//==UserScript==`)? – Rob W Jul 07 '12 at 21:49
  • I'm developing it for chrome. I'm not distributing it or anything. Just something I'm doing on my own as of now, so the meta data would not mean anything. – akagixxer Jul 07 '12 at 21:50

2 Answers2

23

GM_ functions will not work in injected code because injected code runs in the target page's scope. If they did work there, then unscrupulous web-sites could also use the GM_ functions -- to do unspeakable evil.

The solutions, most preferable first:

  1. Don't inject code. Much of the time, it really isn't necessary, and it always complicates things. Only inject code if you absolutely, positively need to use some of the javascript loaded by the target page.

    For libraries like jQuery, you will get better performance using the @require directive (Firefox), or pasting-in the library code or using a custom manifest.json file to include it (Chrome).

    By not injecting code, you:

    1. Keep the ability to easily use GM_ functions
    2. Avoid or reduce the dependency on outside servers to deliver libraries.
    3. Avoid potential side effects and dependencies with/on the page's JS. (You could even use something like NoScript to completely disable the page's JS, while your script still runs.)
    4. Prevent malicious web sites from exploiting your script to gain access to the GM_ functions.

  2. Use the Tampermonkey extension (Chrome). This allows you to avoid script injection by providing better Greasemonkey emulation. You can use the @require directive and a more powerful/risky version of unsafeWindow than Chrome natively provides.

  3. Split your userscript code into injected parts -- which cannot use GM_ functions -- and non-injected parts. Use messaging, polling, and/or a specific DOM node to communicate between the scopes.



If you really must use injected code, here's a sample script that shows how to do it:

// ==UserScript==
// @name        _Fire GM_ function from injected code
// @include     https://stackoverflow.com/*
// @grant       GM_xmlhttpRequest
// ==/UserScript==
/* Warning:  Using @match versus @include can kill the Cross-domain ability of
    GM_xmlhttpRequest!  Bug?
*/

function InjectDemoCode ($) {
    $("body").prepend ('<button id="gmCommDemo">Open the console and then click me.</button>');

    $("#gmCommDemo").click ( function () {
        //--- This next value could be from the page's or the injected-code's JS.
        var fetchURL    = "http://www.google.com/";

        //--- Tag the message, in case there's more than one type flying about...
        var messageTxt  = JSON.stringify (["fetchURL", fetchURL])

        window.postMessage (messageTxt, "*");
        console.log ("Posting message");
    } );
}

withPages_jQuery (InjectDemoCode);

//--- This code listens for the right kind of message and calls GM_xmlhttpRequest.
window.addEventListener ("message", receiveMessage, false);

function receiveMessage (event) {
    var messageJSON;
    try {
        messageJSON     = JSON.parse (event.data);
    }
    catch (zError) {
        // Do nothing
    }
    console.log ("messageJSON:", messageJSON);

    if ( ! messageJSON) return; //-- Message is not for us.

    if (messageJSON[0] == "fetchURL") {
        var fetchURL    = messageJSON[1];

        GM_xmlhttpRequest ( {
            method:     'GET',
            url:        fetchURL,
            onload:     function (responseDetails) {
                            // DO ALL RESPONSE PROCESSING HERE...
                            console.log (
                                "GM_xmlhttpRequest() response is:\n",
                                responseDetails.responseText.substring (0, 80) + '...'
                            );
                        }
        } );
    }
}

function withPages_jQuery (NAMED_FunctionToRun) {
    //--- Use named functions for clarity and debugging...
    var funcText        = NAMED_FunctionToRun.toString ();
    var funcName        = funcText.replace (/^function\s+(\w+)\s*\((.|\n|\r)+$/, "$1");
    var script          = document.createElement ("script");
    script.textContent  = funcText + "\n\n";
    script.textContent += 'jQuery(document).ready( function () {' + funcName + '(jQuery);} );';
    document.body.appendChild (script);
};
Community
  • 1
  • 1
Brock Adams
  • 90,639
  • 22
  • 233
  • 295
  • Thanks! This gave enough to work with and figure things out. I do have to use injected code. In the future I hope to find a more elegant solution but this will work great for now given my limited knowledge on web scripting. – akagixxer Jul 13 '12 at 15:49
  • This is only half of what I need, I also need to get messaging working the other way, and make it a universal drop in for $.ajax This is starting to look really gross. – Ryan Leach Sep 18 '19 at 16:15
  • 1
    @RyanTheLeach, It's not meant to be easy. In addition to the above, you could dispatch custom events and/or [`exportFunction` a helper function](https://stackoverflow.com/a/25785794/331508) or two. – Brock Adams Sep 18 '19 at 16:34
  • I managed to fix my issue, as I had control of the server that I was trying to proxy out to, just setup some CORS headers, easy! I had CORS backwards in my head for some reason, thinking the tampered site needed to allow my site access. exportFunction a helper + custom events would have been the way to go though, after looking through it. – Ryan Leach Sep 19 '19 at 15:55
0

You are posting out to localhost. If you are willing to, (Not everyone will be) setup CORS headers to allow the tampered site access on your localhost server, or all sites if it's something not expected to be secure / ran by others / published.

Then you don't need to use GM_XHR at all, as long as the XHR is properly configured to be cross-domain which you can do by wrapping it in an injected script.

Ryan Leach
  • 4,262
  • 5
  • 34
  • 71