Last modified: 2014-09-21 10:17:35 UTC

Wikimedia Bugzilla is closed!

Wikimedia migrated from Bugzilla to Phabricator. Bug reports are handled in Wikimedia Phabricator.
This static website is read-only and for historical purposes. It is not possible to log in and except for displaying bug reports and their history, links might be broken. See T71924, the corresponding Phabricator task for complete and up-to-date bug report information.
Bug 69924 - [Regression] Wrapping user scripts with "if(window.mw){...}" breaks them on Firefox
[Regression] Wrapping user scripts with "if(window.mw){...}" breaks them on F...
Status: RESOLVED FIXED
Product: MediaWiki
Classification: Unclassified
ResourceLoader (Other open bugs)
1.24rc
All All
: High major with 7 votes (vote)
: 1.24.0 release
Assigned To: Krinkle
: code-update-regression
Depends on:
Blocks:
  Show dependency treegraph
 
Reported: 2014-08-22 22:21 UTC by Fomafix
Modified: 2014-09-21 10:17 UTC (History)
16 users (show)

See Also:
Web browser: ---
Mobile Platform: ---
Assignee Huggle Beta Tester: ---


Attachments

Description Fomafix 2014-08-22 22:21:23 UTC
https://gerrit.wikimedia.org/r/152122 adds an additional

 if(window.mw){...}

around of user scripts. This breaks some scripts in Firefox. Some scripts use forward references of functions like

 foo();
 function foo() {
   console.log( 'foo' );
 }

Forward references normally work, but not in an if block.

 if ( true ) {
   foo();
   function foo() {
     console.log( 'foo' );
   }
 }

generates

 ReferenceError: foo is not defined


A possible workaround is to put this in an additional closure because

 if ( true ) {
   ( function () {
     foo();
     function foo() {
       console.log( 'foo' );
     }
   }() );
 }

works also in Firefox.

Instead of

 if(window.mw){...}

the script has to wrapped with 

 if(window.mw){(function(){...}());}
Comment 1 Michael M. 2014-08-23 09:07:19 UTC
(In reply to Fomafix from comment #0)

> A possible workaround is to put this in an additional closure because
> 
>  if ( true ) {
>    ( function () {
>      foo();
>      function foo() {
>        console.log( 'foo' );
>      }
>    }() );
>  }
> 
> works also in Firefox.

This will break all scripts using var foo = 'bar'; to set global variables, as it will turn them into local ones.
Comment 2 Fomafix 2014-08-23 10:14:38 UTC
> This will break all scripts using var foo = 'bar'; to set global variables,
> as it will turn them into local ones.

Oh, yes. This would generate other problems.
Comment 3 Derk-Jan Hartman 2014-08-23 11:19:13 UTC
I had to fix a user's userscript yesterday due to this bug.
Comment 4 Helder 2014-08-23 12:17:20 UTC
(In reply to Fomafix from comment #0)
> ...
> A possible workaround is to put this in an additional closure ...
For that, see bug 63728.
Comment 5 Fomafix 2014-08-23 14:35:39 UTC
$.globalEval() [1] as suggested in bug 63587 would be a possible solution.

Maybe this can added as a general optional parameter for all modules.

[1] https://api.jquery.com/jQuery.globalEval/
Comment 6 Michael M. 2014-08-25 07:38:23 UTC
Why do user scripts have to be enveloped in that if-clause anyway?

1. In most browsers the global mw will be defined before the user script is executed, so there is no need to test for it.
2. If for some reason the loading order is wrong, it would be a bug, that should be reported, and for this should be visible. Testing for the existence of mw will silently hide such a bug.
3. The only case in which mw will not be defined are unsupported browsers (like IE6) which are blacklisted in the startup function. But if a user of such a browser has a user script that doesn't depend on mw and jQuery, why shouldn't it be executed?

So there is actually no benefit of wrapping user scripts in that if-clause, but several reasons not to do so.
Comment 7 Erik Moeller 2014-08-26 07:50:18 UTC
Michael, if common.js is non-blank, this will lead to site-wide errors in IE6 (and other disabled browsers). That was one of the motivations for Timo's change. But I'll let him comment on strategies to resolve the issue with user scripts.
Comment 9 Michael M. 2014-08-26 08:28:04 UTC
(In reply to Erik Moeller from comment #7)
> Michael, if common.js is non-blank, this will lead to site-wide errors in
> IE6 (and other disabled browsers).

My comment was limited to user scripts, and did not include site-wide scripts. For MediaWiki:Common.js etc. I agree it is the right thing to wrap it into that if-clause. But I don't think it should be done for User:Foo/common.js etc. While most site-wide scripts should be fixed by now, most user scripts aren't, because it is much more difficult to fix lots of user scripts that to fix a few site-wide scripts.

If a user script breaks because it refers to mw without checking for its existence though the user uses a blacklisted browser, it's solely the user's responsibility to fix his own script. But a user script should never break, because the software changes it in a way that changes valid syntax into invalid (as it is in this case, a FunctionDecleration is allowed in global scope, but not in block scope, and the if-clause changes the scope of the script from global to block).
Comment 10 Michael M. 2014-08-26 10:17:08 UTC
As actually nobody explicitly quoted the relevant standards yet:

* ECMAScript defines FunctionDecleration and FunctionExpression. (http://ecma-international.org/ecma-262/5.1/#sec-13)
* Firefox (and other browsers) extend the ECMAScript syntax, for Firefox there is also FunctionStatement. (https://developer.mozilla.org/en-US/docs/Web/JavaScript
/Reference/Functions_and_function_scope#Conditionally_defining_a_function)
* All these look similar, but behave differently.
* User scripts affected by this bug use functions defined as FunctionDecleration.
* In ECMAScript a FunctionDecleration is not allowed inside a block.
* When such a user script is wrapped in an if-clause, Firefox therefor treats the functions as FunctionStatement instead.
* This can cause errors, because functions defined as FunctionStatement can't be called before they are defined, while this is possible for functions defined as FunctionDecleration.
* Additionally FunctionStatements are not valid in strict mode. This isn't a problem here, because even if the script declares strict mode globally, this won't have any effect after it is wrapped in that if-clause (because the 'use strict'; no longer is in the first line then). Nevertheless one shouldn't rely on syntax extensions by different browsers.
Comment 11 Krinkle 2014-08-26 17:24:57 UTC
This is not a new problem. Users using older browsers have always had fatal errors because jquery/mediawiki are not loaded in blacklisted browsers not supporting our required version for the javascript engine. They used to fail on a ReferenceError for $ or mw undefined. This error happens because, unlike modern modules that are only loaded in a supported environment, site/user modules are executed in the global scope for legacy reasons and unconditionally referenced in the HTML.


As we do, we move with the times and naturally raise our requirements as new technologies come available, become dependant on it, and eventually no longer support older engines for javascript features (basic functionality will remain supported in these browsers however!).

Up until recently this applied to MSIE 5, Firefox 2 (and below). This was raised to MSIE 6, Firefox 2 and Opera 11 (and below).

This has exposed the ReferenceError wider. Thus, we've wrapped the site/user module in the same "if(window.mw)" conditional as we've wrapped other scripts for years now (mw.config.set, mw.user.options etc.). This means users in older browsers no longer get errors (especially for site scripts. For user scripts it's a minor issue because they are their own scripts, if they frequently use older browsers, they probably don't need that user script as it wouldn't be executed anyway).

---

We can't wrap the code in a closure because they need to be executed in the global scope. If we can wrap them in a closure, they wouldn't have to be loaded via hardcoded script tags, wrapped in mw.loader.implement closures and we wouldn't have this problem in the first place.

Many of these scripts do indeed have global var statements or function declarations (including relying on function hoisting).

To my knowledge, conditional function declarations (or variable declarations for that matter) are a bit silly because it's unconditionally hoisted.

e.g.

> bar;
>> ReferenceError: bar is undefined

> if (true) {
>   var bar = 5;
> }
> bar;
>> (nothing)

Because 'var bar;' was hoisted.

The same applies to function declarations (executed in V8 under Chrome)

> function bar() { return 1; }
> bar();
>> 1

> if (true) {
>   function bar() { return 1; }
> } else {
>   function bar() { return 2; }
> }
> bar();
>> 2

This is bad practice, but I figured it'd be harmless for an implementation detail like if-window-mw (considering there'd never be any statements before or after the if-statement).
Comment 12 Krinkle 2014-08-26 17:28:17 UTC
> > function bar() { return 1; }
> > bar();
> >> 1
> 
> > if (true) {
> >   function bar() { return 1; }
> > } else {
> >   function bar() { return 2; }
> > }
> > bar();
> >> 2

btw, in Firefox this yields 1.
Comment 13 Krinkle 2014-08-26 17:29:26 UTC
> quux(); if (true) { function quux() { return 1; } } else { function quux() { return 2; } }
V8> 2
SpiderMonkey> quux is undefined
Comment 14 Krinkle 2014-08-27 00:21:47 UTC
Hm.. so yeah, that's annoying. The condition-wrap is supposed to hide an issue in old browsers, not introduce issues in modern browsers.

So far I've only seen two possible solutions:

1) Wrap in $.globalEval, passing the function body as a string.

Something like:

 if (window.mw) {
  $.globalEval('foo(); function foo() { return 1; }');
 }

Produced by

  $content = 'if (window.mw) { $.globalEval(' . FormatJson::encode( $content ) . ' ); }';';

Or:

  $content = 'if (window.mw) { ' . Xml::encodeJsCall( '$.globalEval', array( $content ) ) . '}';';

Though this is relatively cheap and doesn't bloat the output much in terms of size, it does make it impossible to minify after the fact, so we should probably run the js-filter on it first.

2) Parse the javascript and transform local var statements and function declarations to explicit instead of implicit global properties so that scope doesn't matter. This can be done with something like emscripten, and then use the AST to replace var statements and function declarations with window[key] assignments (and manually hoist function declarations I guess). Then it's safe to use a closure (or even a plain wrap, like now).

This requires getting a JS parser in PHP though, or shelling out to nodejs.
Comment 15 Krinkle 2014-08-27 00:22:28 UTC
Open to other ideas. Of neither #1 or #2 is acceptable, we may have to remove the if-wrap for the time being since modern Firefox triumps priority over grade C browsers.
Comment 16 Michael M. 2014-08-27 09:25:55 UTC
What about only wrapping the
 mw.loader.state({"user":"ready"});
at the bottom in an if-clause (or prefixing it with window.mw && ) and leaving the rest of the script alone? I hadn't seen that call to mw.loader.state when I wrote comment 6, but I still think that my general remarks apply:

If a user script throws a ReferenceError because it uses mw or $, but the user uses a blacklisted browser, it's the user's fault, and nothing MediaWiki needs to try to work around. And if it doesn't use mw or $, there is no reason not to execute it.

This still leaves the question whether site-wide scripts should be wrapped. If admins fix errors with these scripts quickly, it is better to wrap them (as errors in Firefox are noticed much faster than errors in IE6), but if admins don't fix errors, it would be better not to wrap them (as errors in IE6 are less important than errors in Firefox). It seems that the errors in site-wide scripts were fixed quite fast in the last few days, so I think the if-clause is ok for site-wide scripts.
Comment 17 Krinkle 2014-08-27 13:14:25 UTC
Alternatively, we might be able to do the condition outside the script. Perhaps something like replace this:

<script src="./load.php?modules=user&only=scripts&user=Example&version=1"></script>

with:

<script>if (cond) document.write("\u003Cscript src=\"./load.php?modules=user\u0026only=scripts\u0026user=Example\u0026version=1\"\u003E\u003C/script\u003E");</script>

Of course, document.write is horrible, but nothing about this is pretty. document.write can be unreliable, but there are certain scenarios in which it is deterministically and cross-browser safe to use. I wonder if this is such case.
Comment 19 Krinkle 2014-08-27 23:57:17 UTC
Another solution would be to return early instead of wrap the whole thing.

While a return statement is not allowed in the global scope, a throw works fine.


 if (!cond)
  throw '';
 .. rest of script ..
 .. mw/$ ..

This doesn't cause much disruption since javascript keeps running. The throw only aborts the current script, which is the user/site script we want to unload.
Comment 20 Krinkle 2014-08-28 00:11:42 UTC
(In reply to Krinkle from comment #19)
> Another solution would be to return early instead of wrap the whole thing.
> 
> While a return statement is not allowed in the global scope, a throw works
> fine.
> 
> 
>  if (!cond)
>   throw '';
>  .. rest of script ..
>  .. mw/$ ..
> 
> This doesn't cause much disruption since javascript keeps running. The throw
> only aborts the current script, which is the user/site script we want to
> unload.

Hm.. continuing the monolog; while exceptions are harmless indeed, that's what we started with. Execute the script and let it fail on mw/$ undefined.

This wasn't really a problem. Except that old IE tends to actually expose script errors to regular users through an alert, so I guess we can't do this.
Comment 21 Bartosz Dziewoński 2014-08-28 20:11:35 UTC
I quite like the document.write one, seems it will have the least impact on users with non-broken user JS and should be easy to implement?
Comment 22 PDD 2014-09-05 01:17:39 UTC
Erm, so something was introduced to deal with users of outdated browsers (say, 1 % of our users) and that something broke something else for users of non-outdated browsers (Firefox, about 20 % I guess?) and instead of fixing the problem immediately, what happens is... nothing? Hello???
Comment 23 Andre Klapper 2014-09-08 15:21:23 UTC
Krinkle: So is there some conclusion what should be done here, and by who?
Comment 24 Krinkle 2014-09-08 15:30:03 UTC
I've proposed various solutions. None of which are field-tested, best practice and known to work cross-browser.

The inline doc.write solution seems the most straight forward. I'll implement that and test it in as many browsers as I can. While we're already using doc.write in other places, embedding it in the HTML can have side-effects with regards to caching and/or trigger weird bugs (e.g. old IE is known to have incompetent security policies that are undocumented and not compatible with "the web" and can cause semi-random things to fail sometimes).
Comment 25 Gerrit Notification Bot 2014-09-08 17:02:04 UTC
Change 159086 had a related patch set uploaded by Krinkle:
resourceloader: Do condition wrap in HTML instead of JS response

https://gerrit.wikimedia.org/r/159086
Comment 26 Gerrit Notification Bot 2014-09-13 13:29:10 UTC
Change 159086 merged by jenkins-bot:
resourceloader: Condition-wrap the HTML tag instead of JS response

https://gerrit.wikimedia.org/r/159086
Comment 27 Bartosz Dziewoński 2014-09-13 13:38:31 UTC
This is marked as "High major", so I assume that the fix will be deployed by someone on Monday?
Comment 28 Greg Grossmeier 2014-09-15 15:40:12 UTC
Bartosz: I'm just seeing this now. No one added it this morning; you should have :)
Comment 29 Bartosz Dziewoński 2014-09-21 10:17:35 UTC
It wasn't backported in the end :( The fix will roll out with 1.24wmf22.

Note You need to log in before you can comment on or make changes to this bug.


Navigation
Links