Greasemonkey: Can't access variables defined in <script> tags on the actual page.

Created on 18 Nov 2017  路  14Comments  路  Source: greasemonkey/greasemonkey

With the update to Greasemonkey 4, many userscripts using jQuery seem to have been broken. In the previous versions, the following code would work on a page using jQuery in a <script> tag:

// ==UserScript==
// @name        Variable access test
// @namespace   example.com
// ==/UserScript==

$.ready(document, function() {
    console.log("Accessed jQuery successfully.")
});

However, that doesn't work anymore - instead, you just get a classic $ is not defined. That tells me Greasemonkey might be running the userscript before the <script> tag gets loaded. That's odd, however, seeing as the script still runs when document.readyState is interactive!

To be sure, though, I tried accessing jQuery after a window.addEventListener('load', ...). Lo and behold, the following exhibits a different error:

// ==UserScript==
// @name        Variable access test
// @namespace   example.com
// ==/UserScript==

window.addEventListener('load', function() {
    console.log("Before accessing jQuery")
    $;
    console.log("Accessed jQuery successfully!")
});

It neither runs nor throws an error - instead, it freezes when trying to access $. The script simply runs up to that line and no further.


Try it on any page that uses jQuery and you should get the same result. Using @require is not a solution to this problem - for some applications, you need access to the same instance of jQuery as the page. This most likely affects scripts other than jQuery, too.

duplicate

Most helpful comment

I've tried that approach now (in different configurations), and I keep getting permission errors whatever I do. It really isn't worth the hassle at this point.

It sounds harsh, but I think I'll just have to recommend people use Tampermonkey until this is out of the way. Content scripts, in their current incarnation, really don't fit the userscript use case very well at all.

All 14 comments

Not really a Greasemonkey issue, more of a Firefox content script issue.
https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Content_scripts#Accessing_page_script_objects_from_content_scripts

Either assign your own window.wrappedJSObject or use unsafeWindow in which Greasemonkey has already assigned the wrapped object.

@Sxderp I don't know about "not really a Greasemonkey issue" - both Tampermonkey and Violentmonkey handle this very case without problems. Not only that, but this is a breaking change that isn't mentioned in any of the blog posts.

In order to work as expected with existing userscripts and to be cross-compatible with other popular userscript managers, shouldn't this be fixed?

Also... does this really explain the issue? Why would the script freeze and not just have a failed lookup?

I'm not sure about the freezing. Copied your code and it doesn't freeze for me.

Edit: Sorry, misunderstood the 'freeze' comment. I thought you meant Greasemonkey doesn't work anymore. It 'freezes' (stops executing) at that line because Javascript hits a use before declaration error. Pretty sure that's considered 'fatal'. As in, doesn't continue executing code.


Taking a quick look at Violentmonkey code, it looks like they inject scripts by creating <script> elements. Rather than using the tabs.executeScripts() API method.

Using that has its own drawbacks too, of course.

So... when Firefox runs into that line, it just silently stops executing? Without any error message? What an incredibly strange thing to do.

In any case, is it not a goal to maintain compatibility with existing userscripts? This isn't a niche feature, after all - interfacing with the page's own JavaScript is a very common thing to do.

In any case, is it not a goal to maintain compatibility with existing userscripts?

Of course it is. We're not perfect, and this is a free open source project created by volunteers with only so much free time.

At this point, I've got a plan for how to improve this, but it's waiting on Mozilla to implement a feature that would make it possible to do securely. #2549

@arantius I think I might've come off a bit confrontatively... Sorry about that.

I'm guessing it's a thought-out decision to prioritize security over usability here. Until the situation gets resolved, what can we do as a workaround on the script author side of things? In my specific case, I tried both var $ = window.wrappedJSObject.$; and var $= unsafeWindow.$; at the top of a script, but that gave me another error upon trying to use $(document).ready(...): Error: Permission denied to access property Symbol.toStringTag.

Error: Permission denied to access property Symbol.toStringTag

I don't think that's a problem of the window.wrappedJSObject.$ but rather some other object that you created in the userscript, either as a object literal or with the new constructor, has not been properly imported into scope in which you're trying to access it (probably the page's window scope).

The link above provides details on cloneInto which is waht you'll need.

No, I'm fairly certain the problem is with window.wrappedJSObject.$. The following fails with that very same error:

// ==UserScript==
// @name        Variable access test
// @namespace   example.com
// ==/UserScript==

var $ = window.wrappedJSObject.$;

$(document).ready(function() {});

This, however, fails silently:

// ==UserScript==
// @name        Variable access test
// @namespace   example.com
// ==/UserScript==

var $ = window.wrappedJSObject.$;

$.ready(function() {});

I've tried using cloneInto, but it doesn't help. Do you think you could provide a short example userscript which uses the page's instance of $?

Ah, in that case you don't have permission to access the function. You then need to use exportFunction.

// ==UserScript==
// @name         ExampleJQuery
// @version      1
// @include      *
// @grant        none
// ==/UserScript==

var unsafeWindow = window.wrappedJSObject;
var $;

// For sanity just return if we don't have the object
if (typeof unsafeWindow.$ === 'undefined') {
  console.log('No jQuery object, returning');
  return;
} else {
  $ = unsafeWindow.$;
}

// Create the function we want to export
function onReady() {
  console.log("I'm ready!");
}
// Export it. Some details on this.
// Argument 1. The function to export
// Argument 2. The scope to export it to. In general this will be window,
//             or some object in the scope. While it is valid to use
//             "window", I prefer to use "unsafeWindow" if I'm exporting
//             into that scope. I find it less confusing.
// Return      This is a reference to exported function. In general you'll
//             assign this to some property of the scope you're exporting
//             into. However, it's not always neccessary. For example, if
//             you're going to use it as a callback (like for .ready())
//             then you don't need to assign it into the exported scope.
//             But if you want it globally (or scopally) accessable then
//             you need to assign it.
let exported_onReady = exportFunction(onReady, unsafeWindow);
// OR
// unsafeWindow.onReady = exportFunction(onReady, unsafeWindow);

$(document).ready(exported_onReady);
// OR
// $(document).ready(unsafeWindow.onReady);

I've tried that approach now (in different configurations), and I keep getting permission errors whatever I do. It really isn't worth the hassle at this point.

It sounds harsh, but I think I'll just have to recommend people use Tampermonkey until this is out of the way. Content scripts, in their current incarnation, really don't fit the userscript use case very well at all.

Agree with @obskyr, just recomment my people to use tampermonkey

+1, moved to Tampermonkey (https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/), seems like all my scripts work there.

+1, moved to Tampermonkey (https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/), seems like all my scripts work there.

This may work in the short term, and it works fine to keep scripts running, but in the long run, it could be a false decision to rely on a complete substitution. The short term success may be short sighted... ;-)

I use TM as a substitute, but there are some issues with it, I can't deny. Additionally, there may be some security issues with FF Quantum, so it works, but one should not rely on a single solution only...

Well, I need to support TM in my scripts anyway because not everyone uses Firefox. So before I was supporting GM and TM, now it's just TM. Didn't notice any issues so far.

Was this page helpful?
0 / 5 - 0 ratings