icecub
icecub

Reputation: 8773

function not defined or permission denied to access object in Greasemonkey

I'm working on a Greasemonkey script that injects a button into a chatsystem (Gitter) which allowes you to send default messages. (Not to spam. It's for Admins to send messages like a Code of Conduct).

Let's assume I've injected a button:

<button onclick='setTimeout( function() { showMsg() }, 10 );'>Send default message</button>

Following the advise from this question: Javascript: Function is defined, but Error says.. Function is not found ! (Strange) to use setTimeout() because functions defined in GM are not available to the main Window.

For testing purposes, the function is a simple console log:

function showMsg(){
    console.log('showMsg triggered');
}

The problem is that this still returns me a "showMsg is not defined". So following the other adivse provided in the question mentioned earlier, I tried:

unsafeWindow.showMsg = function(){
    console.log('showMsg triggered');
}

This however returns me:

Error: Permission denied to access object

Strangely enough, it seems that this only occurs when creating a div element and using innerHTML to inject the button into that div. When actually creating a button like this:

var btn = document.createElement('button');
btn.onclick = function(){ showMsg(); };

It works as expected.

Some additional information that might help find an answer:
Gitter chat uses an iframe for the chat messages and the message input field. The iframe is on the same server, so there's no Same Original policy issue.

The button is injected into the main body. Here are 2 example's as to how I've tried injecting them.

(Working version)

var iframe, iframeDoc;
var chatContainer;
var chatInput, chatText;
var msgMenu;

$(document).ready(function() {
    iframe = document.getElementById('content-frame');

    iframe.onload = function(){
        iframeDoc = iframe.contentDocument || iframe.contentWindow.document;

        chatContainer = iframeDoc.getElementById('chat-container');
        chatInput = iframeDoc.getElementById('chat-input');
        chatText = iframeDoc.getElementById('chat-input-textarea');

        loadButton();
    }
});

function loadButton(){
    var div = document.createElement('div');
    var btn = document.createElement('button');
    var btnText = document.createTextNode('Send default message');

    // Lots of CSS here that ads absolute positioning
    // and makes the div float in the middle of the screen

    btn.onclick = function(){ showMsg(); };

    btn.appendChild(btnText);
    div.appendChild(btn);

    div.setAttribute("id", "msgMenu");

    document.body.appendChild(div);
    msgMenu = document.getElementById('msgMenu');
    return false;
}

function showMsg(){
    console.log('showMsg triggered');
}

When changing the loadButton function to this, it's causing the issues mentioned:

function loadButton(){
    var div = document.createElement('div');

    div.innerHTML = "<button onclick='setTimeout( function() { showMsg() }, 10 );'>Send default message</button>";

    div.setAttribute("id", "msgMenu");

    document.body.appendChild(div);
    msgMenu = document.getElementById('msgMenu');
    return false;
}

Upvotes: 4

Views: 3397

Answers (1)

JRI
JRI

Reputation: 1942

Your userscript is a "content script", that runs in the context of the Greasemonkey extension (or other userscript manager), and has access to the Greasemonkey API. It is called a content script because it can access the content of the web page, i.e. the DOM.

The scripts that are called up from the web page's HTML are "page scripts", and run in a separate context. They can't directly access the scope of content scripts, including your userscript.

In both your examples, the showMsg() function exists only in the scope of the content script. In the working example, the code that attaches the anonymous function to the button's onclick handler executes in the same scope, where showMsg() is visible. A reference to the anonymous function is attached to the <button> node, creating a closure that causes it to persist in memory even after the main userscript has finished executing. The closure also means that the function has access to the same objects that were in scope when the function was defined, including showMsg() (and the GM API objects).

In the second example, the button is created by evaluating the innerHTML of its parent <div> in the page script scope. This means that showMsg() is not visible at the point that the click handler is attached to the button, giving the "not defined" error when the handler is called.

The problem isn't that you used innerHTML to inject the button; it's that you used it to attach the click handler. This code also works, using innerHTML to create the button, but attaching the handler from the content script scope:

function loadButton(){
  var div = document.createElement('div');

  div.innerHTML = "<button id='myButton'>Send default message</button>";
  document.body.appendChild(div);

  msgMenu = document.getElementById('msgMenu');
  document.getElementById('myButton').addEventListener('click', showMsg);
  return false;
}

I can't explain exactly why you are getting the error message with unsafeWindow, but if you are using Greasemonkey v4 or above, and/or Firefox v57 or above, it is likely to be due to recent changes to how Firefox handles extensions. See MDN and the Greasemonkey blog for more clues.

If you do need to add functions to the web page itself, a relatively simple and reliable way is to inject a <script> node into the DOM. Note that the inserted functions won't have access to anything from the userscript's scope. There's more documentation on MDN describing more complicated ways of interacting with page scripts, but some of these only work with Firefox.

Upvotes: 10

Related Questions