Fix annotating sites with broken Function.bind polyfills#315
Fix annotating sites with broken Function.bind polyfills#315
Conversation
src/annotator/annotation-sync.coffee
Outdated
| iframe = document.createElement('iframe') | ||
| iframe.style.display = 'none' | ||
| document.body.appendChild(iframe) | ||
| bind = iframe.contentWindow.Function.prototype.bind |
There was a problem hiding this comment.
Should we move this bind into some standard place that different files can import it from?
There was a problem hiding this comment.
I think this would make sense. It seems likely that we might want to do this in other places in future. It should also be much easier to test if you extract it out into a separate module.
| createAnnotationSync(); | ||
|
|
||
| publish('deleteAnnotation', {msg: ann}, function() {}); | ||
| function addTests() { |
There was a problem hiding this comment.
The problem here is that I want to run all of AnnotationSync's tests in two contexts: with and without a broken Function.bind in the parent page. Maybe not all the tests really need to be run with the broken Function.bind, but a few of them should be, and this avoids duplicating them.
It does add some "cleverness" to the test code though.
An alternative would be to just add one test for the broken Function.bind context: that the constructor doesn't crash. Since the broken Function.bind that the tests are using is one that crashes when called, just this one test would prove that AnnotationSync doesn't use the document's Function.bind
|
Most of the tests diff that you see is just due to code having been indented. I didn't add or change any tests. I just wrapped all the tests in an As noted above, I think it might be better just to add a single "it does not crash if the parent page contains a crashing Function.bind polyfill" test, instead of doing this |
32c37d4 to
b2caed7
Compare
|
Ok, I've moved the clean function.Bind into a clean-context util module, and I've replaced the test cleverness discussed above. Ready for review again |
| @@ -0,0 +1,22 @@ | |||
| /** | |||
| * Export clean versions of standard functions and objects. | |||
| * | |||
There was a problem hiding this comment.
I think we need to be a bit clearer about the motivation here and what "clean" means. Specifically that this module is a workaround for environments that overwrite properties on global objects with broken polyfills or other problematic values.
There was a problem hiding this comment.
I've edited the paragraph below to make this more explicit
src/annotator/util/clean-context.js
Outdated
| * This module exports "clean" versions of various standard functions that may | ||
| * have been modified in the parent browsing context that the client is | ||
| * injected into. Code should always use the clean versions from this module, | ||
| * and never the potentially modified versions from the parent context. |
There was a problem hiding this comment.
Need to clarify what "Code" means here. Specifically you mean code in the "annotator/" dir, as this isn't relevant for the sidebar.
There was a problem hiding this comment.
Done, although I don't want to mention the annotator dir by name here as that comment seems unlikely to be updated if we rename it
src/annotator/util/clean-context.js
Outdated
| // browsing context to extract clean objects and functions from. | ||
| var iframe = document.createElement('iframe'); | ||
| iframe.style.display = 'none'; | ||
| document.body.appendChild(iframe); |
There was a problem hiding this comment.
On my shiny new Mac I measured the cost of creating an iframe as being ~6-13ms, so probably up ~100ms on a mobile device. It would be nice not to do this for the vast majority of pages that are not going to need it.
I've asked around whether there is a cheaper way, but an approach I'm pretty sure will work fine for feature detection is just to check whether the stringified function (via fn.toString()) contains the text [native code]. This is currently a convention but will most likely be part of the language spec in future.
There was a problem hiding this comment.
That looks like it's "NativeFunction" rather than "[native code]"? But is there any guarantee that a page's replacement of a native function won't stringify to "NativeFunction" as well?
There was a problem hiding this comment.
@seanh "NativeFunction" in the spec is a reference to another part of the spec. Search for other occurrences of "NativeFunction" in that document :)
There was a problem hiding this comment.
Yes it is technically possible for a page to overwrite toString() with something which always returns function () { [native code] } but I think it is unlikely enough that we'll be OK doing that.
There was a problem hiding this comment.
Or of someone just modifies Function.prototype.bind.something but doesn't change toString?
Let's see if we run into anyone that does it, I guess. I think this clean context thing is a temporary patch (before moving all of src/annotator into a same-origin iframe) anyway.
If we want to move the whole thing into an iframe by the way, when we do that we will have to eat this 6-100ms delay every time anyway won't we?
There was a problem hiding this comment.
Done - I've changed it to not inject an iframe if it doesn't think bind has been modified
|
I tested this by overwriting |
robertknight
left a comment
There was a problem hiding this comment.
This approach works fine. Three requests:
- Creating an iframe is expensive, we should avoid it when not needed
- The documentation needs to clarify what "clean" means and which code this applies to
- I found a couple more places in the sidebar app that use
bind()that will still call the broken version.
b2caed7 to
31458f8
Compare
Fixed these two as well |
Add failing tests for bugs in annotation-sync.js and sidebar.coffee when the parent document contains a broken Function.bind polyfill. See #245
Add a module that uses an invisible same-origin iframe to return clean copies of global function and objects if the parent browsing context's copies have been modified. So far it contains only a clean copy of Function.bind, but more can be added. Also change other modules to always use cleanContext.bind and never Function.bind directly. Fixes #245
31458f8 to
ff60dc9
Compare
Codecov Report
@@ Coverage Diff @@
## master #315 +/- ##
==========================================
+ Coverage 76.48% 76.73% +0.24%
==========================================
Files 120 121 +1
Lines 5886 5918 +32
Branches 959 964 +5
==========================================
+ Hits 4502 4541 +39
+ Misses 1384 1377 -7
Continue to review full report at Codecov.
|
| }); | ||
| }); | ||
|
|
||
| context('if the parent context has a crashing Function.bind polyfill', function() { |
There was a problem hiding this comment.
I've left this test (and the one for sidebar below) here as I can't think of a better way. This obviously isn't ideal as every module that uses clean-context would need to have its own "doesn't crash" test.
I looked into replace Function.prototype.bind globally with a crashing function in the tests, but don't see any way to do that for all non-sidebar tests but not do it for the src/sidebar tests.
I looked into writing an eslint rule to warn on uses of bind, but it seemed difficult to warn when bind() or bind.call() is called but not when the calling code is in src/sidebar and not when cleanContext.bind.call() is called. (It may be doable, I've not written eslint rules before, but there wasn't an obvious good way to do it.)
The remaining option would be to delete these tests, but that would simply mean that the actual bug fixes in this pull request would not be covered by tests. I don't think the presence of these tests is doing any particular harm or means that we'd necessarily have to add similar tests for every other piece of code that uses clean-context.
|
@robertknight This can be reviewed again. My one remaining concern is whether CSP can prevent the iframe from being injected? I tested in Chrome using the Chrome extension on a page with The bookmarklet fails on that page (either Firefox or Chrome) but that's not this branch's fault - app.html runs afoul of the page's content security policy, If I set an So I don't think this branch breaks anything. But what I'd really like to do to be safer is catch this CSP |
|
(My theory by the way is that since the iframe has no |
|
Closed in favour of #331 |
Fixes #245