Replacing page functions via userscript in Chrome? - google-chrome

I've created a Greasemonkey script which replaces a page function:
(function() {
var oldFunc = func;
func = function() {
oldFunc();
// other stuff
};
)();
I had always intended this script to work in both Gresemonkey and Chrome and had read that Chrome doesn't support unsafeWindow, so have been injecting the script into the page by converting the function to a string and using setTimeout:
setTimeout("(function(){var old=func;func=function(){old();/*other stuff*/}}())", 0);
This works perfectly well in Greasemonkey, but gives me absolutely nothing in Chrome. It clearly isn't firing, but nothing appears in the log, either. I have tried a number of other injection styles, but can't get anything to work. (For testing, I also added an alert() to the top of the function.)
setTimeout(..., 0) — nothing
Running the function in-scope, without injection into the page — no alert, but "func is not defined" in console
document.href = "javascript:..." — nothing
document.body.addEventListener("load", ...) — nothing
All of these are with #run-at document-end.
Unfortunately, this being my first Chrome userscript, I'm at a loss as to how to debug it, even after an hour or so of Googling and poking around SO. Any help?
Actual script, currently working in Greasemonkey: http://pastebin.com/HtLVjYHg

In the end, the only technique I found which worked in Chrome was to create a <script> node using DOM methods. Fortunately, this seems to work equally well in Firefox and Opera and is made fairly painless by <func>.toString():
function embed() {
var oldFunc = window.func;
window.func = function() {
oldFunc();
// other stuff
};
}
var inject = document.createElement("script");
inject.setAttribute("type", "text/javascript");
inject.appendChild(document.createTextNode("(" + embed + ")()"));
document.body.appendChild(inject);
Final script: http://userscripts.org/scripts/review/84394

Related

google.script.run in html UI doesn' t call server function

I have a couple of scripts that don't work anymore although I didn't make any change.
I don't spend a lot of time programming these last month so I didn't notice it right away...
Here is a simplified code that used to work and that doesn't, I can't figure out my error.
The HTML part is entirely built on server side since it's a very simple code (runs from a spreadsheet).
function myFunction() {
var doc = '<body style="font-family:arial,sans;font-size:12pt">';
doc+='<select id="target" multiple style="font-family:arial,sans;font-size:12pt">';
doc+='<option value="1">choice 1</option><option value="2">choice 2</option><option value="3">choice 3</option></select><br><br>';
doc+='<input type="button" onClick="processJS()" style="background:#BFA;font-size:12pt" value="Validate"/>';
doc+='<script>function processJS(){var e=document.getElementById("target");var values=Array.from(e.selectedOptions).map(option => option.value);';
doc+='console.log(JSON.stringify(values));google.script.run.withSuccessHandler(function(msg){ window.alert(msg);google.script.host.close;}).processGSTest(values)};</script>';
doc+='</body>';
var ui = HtmlService.createHtmlOutput(doc).setWidth(500).setHeight(250);
SpreadsheetApp.getUi().showModelessDialog(ui, "test");
}
function processGSTest(values){
Logger.log(JSON.stringify(values));
return "processGSTest ok";
}
I get an error in JS console like this when I hit "validate" :
the HTML rendered window looks like this :
EDIT
Following comments (thanks again) I've got a few more informations :
when using another browser the scripts works fine
when using chrome and adding an withFailureHandler it still fails with the same console message without handling the failure (Chrome Version 81.0.4044.122 (Build officiel) (64 bits) on Mac OS High Sierra)
when calling the same function from a custom menu the script works fine even in the same Chrome session
conclusion of this edit : I don't understand the issue :)
Since this post is about a very specific case that does not seem to be reproductible I answer it so it doesn't stay open indefinitely.
In the same time I'll post an issue report on the tracker.
Here is the script I use to show the issue.
The main function (myFunction) reports an error when called from the script editor but works when called from the menu...
This happens only on Chrome Version 81.0.4044.122 (Build officiel) (64 bits) on Mac OS High Sierra 10.13.6, it works normally on Safari, Firefox and even Chrome on Windows 10.
(It works also normally on the same version of Chrome under Mac OS 10.10.5 !! )
function myFunction() {
var doc = '<body style="font-family:arial,sans;font-size:12pt">';
doc+='<select id="target" multiple style="font-family:arial,sans;font-size:12pt">';
doc+='<option value="1">choice 1</option><option value="2">choice 2</option><option value="3">choice 3</option></select><br><br>';
doc+='<input type="button" onClick="processJS()" style="background:#BFA;font-size:12pt" value="Validate"/>';
doc+='<script>function processJS(){var e=document.getElementById("target");var values=Array.from(e.selectedOptions).map(option => option.value);';
doc+='console.log(JSON.stringify(values));google.script.run.withFailureHandler(function err(){window.alert("error triggered by withFailureHandler");})';
doc+='.withSuccessHandler(function(msg){ window.alert(msg);google.script.host.close();}).processGSTest(values)};</script>';
doc+='</body>';
var ui = HtmlService.createHtmlOutput(doc).setWidth(500).setHeight(250);
SpreadsheetApp.getUi().showModelessDialog(ui, "test");
}
function processGSTest(values){
Logger.log(JSON.stringify(values));
return "processGSTest ok";
}
function onOpen(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var menuEntries = [{name: "test", functionName: "myFunction"}]
ss.addMenu("test",menuEntries);
}
I created a shared spreadsheet for anyone that would want to play with it (read only, make a copy to use)

Polymer testing: too much recursion?

I am currently testing a polymer element and need to wait for a variable to be set in my element. After searching how to instruct Javascript to wait for the variable to be set, I came up with the following code:
var behavior;
setup(function(){
behavior = fixture("behavior");
});
test('Behavior loads resources', function(done) {
var waitForI18n = function() {
if(behavior.isI18nLoaded){
clearInterval(interval);
expect(behavior.getKey("test")).to.be.equal("test" + behavior.language.toUpperCase());
done();
}
};
var interval = setInterval(waitForI18n, 50);
});
This works in Chrome but other browsers just freeze and eventually crash: IE11, Edge, Firefox.
I was able to get a "Too much recursion" error in firefox, but not much else.
Am I doing anything wrong? Any ideas?
I've tried with a recursive setTimeout, but the behavior was the same: Chrome works, but not other browsers.

Detect if another browser tab is using speechRecognition

Is it possible to tell if another Chrome tab is using webkitSpeechRecognition?
If you try to use webkitSpeechRecognition while another tab is using it, it will throw an error "aborted" without any message. I want to be able to know if webkitSpeechRecognition is open in another tab, and if so, throw a better error that could notify the user.
Unless your customer is on the same website(you could check by logging the ip/browserprint in database and requesting by json) you cannot do that.
Cross domain protection is in effect, and that lets you know zilch about what happens in other tabs or frames.
I am using webkitSpeechRecognition for chrome ( does not work on FF) and I faced same issues like multiple Chrome tabs. Until the browser implement a better error message a temporary solutions that work for me:
You need to detect when a tab is focused or not in Chrome using
Javascript.
Make javascript code like this
isChromium = window.chrome;
if(isChromium)
{
if (window.addEventListener)
{
// bind focus event
window.addEventListener("focus", function (event)
{
console.log("Browser tab focus..");
recognition.stop();// to avoid error
recognition.start();
}, false);
window.addEventListener("blur", function (event)
{
console.log("Browser tab blur..");
recognition.stop();
}, false);
}
}
There's a small workaround for it. You can store the timestamp in a variable upon activating SpeechRecognition and when it exits after a few seconds of inactivity, it will be compared to a timestamp since the SpeechRecognition was activated. Since two tabs are using the API simultaneously, it will exit immediately.
For Chrome, you can use the code below and modify it base on your needs. Firefox doesn't support this yet at the moment.
var transcriptionStartTime;
var timeSinceLastStart;
function configureRecognition(){
var webkitSpeechRecognition = window.webkitSpeechRecognition || window.SpeechRecognition;
if ('webkitSpeechRecognition' in window) {
recognition = new webkitSpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = "en-US";
recognition.onend = function() {
timeSinceLastStart = new Date().getTime() - transcriptionStartTime;
if (timeSinceLastStart < 100) {
alert('Speech recognition failed to start. Please close the tab that is currently using it.');
}
}
}
}
See browser compatibility here: https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition

Chrome extension: (DOM)Debugger API does not work anymore

Our chrome extension does not work correctly anymore since version 37.0.2062.103 (It used to work correctly on chrome version 36.0.1985.143).
Specifically, the debugger API has stopped working for us when we use the DOMDebugger.
See the attached code: (background.js)
chrome.tabs.onUpdated.addListener(function(tabId,changeInfo,tab){
if( changeInfo.status == "loading" && tab.active){
var debugId = {tabId:tabId};
chrome.debugger.attach(debugId, '1.0', function() {
chrome.debugger.sendCommand(debugId, 'Debugger.enable', {}, function() {
chrome.debugger.sendCommand(debugId, "DOMDebugger.setEventListenerBreakpoint", {'eventName':'click'},
function(result) {
console.log('registering click');
});
});
});
}
});
chrome.debugger.onEvent.addListener(onEvent);
function onEvent(debuggeeId, method,params) {
if(method=="Debugger.paused"){
console.log('DONE!');
}
};
The extension successfully starts the debugger. we get the yellow debugger ribbon.
We also see the 'registering click' msg in the console. the result argument is an empty object {} (line 8).
However upon clicking on a button that has a click event listener nothing happens.
It used to work without any issues.
It seems like it regressed with https://codereview.chromium.org/305753005. One needs to call "DOM.enable" for it to work now. On the Chrome side, we should implicitly enable DOM domain upon setEventListenerBreakpoint for backwards compatibility. Unfortunately it already squeezed into the stable release.

How do you detect that a script was loaded *and* executed in a chrome extension?

I've been tracking down a bug for days... then I realized the bug was me. :/
I had been using webRequest.onComplete, filtered for scripts. My error was that I made the incorrect association between the scripts being loaded and being executed. The get loaded in a different order than they get executed, and thus the timing of the events is not in the order I need them in. I need to inject between certain scripts so I need an event right after a file has been executed and before the next one.
The only solution I can think of at the moment is to alter the JS being loaded before it gets executed. But it makes my stomach turn. And the bfcache would wreak even more havoc, so not a great solution either.
I would use the HTML5 spec's afterscriptexecute, but that is not implemented in Chrome. Is there another API, perhaps an extension API that I can use?
Note: This method no longer works as of Chrome 36. There are no direct alternatives.
Note: The answer below only applies to external scripts, i.e. those loaded with <script src>.
In Chrome (and Safari), the "beforeload" event is triggered right before a resource is loaded. This event allows one to block the resource, so that the script is never fetched. In this event, you can determine whether the loaded resource is a script, and check whether you want to perform some action
This event can be used to emulate beforescriptexecute / afterscriptexecute:
document.addEventListener('beforeload', function(event) {
var target = event.target;
if (target.nodeName.toUpperCase() !== 'SCRIPT') return;
var dispatchEvent = function(name, bubbles, cancelable) {
var evt = new CustomEvent(name, {
bubbles: bubbles,
cancelable: cancelable
});
target.dispatchEvent(evt);
if (evt.defaultPrevented) {
event.preventDefault();
}
};
var onload = function() {
cleanup();
dispatchEvent('afterscriptexecute', true, false);
};
var cleanup = function() {
target.removeEventListener('load', onload, true);
target.removeEventListener('error', cleanup, true);
}
target.addEventListener('error', cleanup, true);
target.addEventListener('load', onload, true);
dispatchEvent('beforescriptexecute', true, true);
}, true);
The dispatch times are not 100% identical to the original ones, but it is sufficient for most cases. This is the time line for the (non-emulated) events:
beforeload Before the network request is started
beforescriptexecute Before a script executes
afterscriptexecute After a script executes
onload After the script has executed
Here's an easy way to see that the events are working as expected:
window.addEventListener('afterscriptexecute', function() {
alert(window.x);
});
document.head.appendChild(document.createElement('script')).src = 'data:,x=1';
document.head.appendChild(document.createElement('script')).src = 'data:,x=2';
The demo can be seen live at http://jsfiddle.net/sDaZt/
I'm not familiar with Chrome Extensions (only browser javascript), but I think that you will unfortunately have to edit your loaded JS so that is calls a function of your choice when it is executed, if you want to do this nicely. This it what Google does for asynchronously loading its Maps Javascript file:
function loadScript() {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "http://maps.googleapis.com/maps/api/js?sensor=false&callback=executed";
document.body.appendChild(script);
}
function executed() {
/* Google maps has finished loading, do awesome things ! */
}
If you really don't want to edit your loaded JS files, you could have a setInterval (or a recursive function with setTimeout) checking regularly if some functions or variables are initialized.
Have you tried script loading using Modernizr.js?
I had a similar issue, where the timing of script loading was causing conflict. I used Modernizr.js, which includes the library yepnope.js by default. Below is an example of some scripts I loaded conditionally. You can include a test clause, or simply load them in the order you prefer, with the guarantee that they will load and execute in the order you wish due to the callback.
Here is an example with a conditional clause:
Modernizr.load({
test: false, //Or whatever else you'd like. Can be conditional, or not so conditional
yep: {
'script1': 'MyJavascriptLibrary1.js'
},
nope: {
'script2': 'MyJavascriptLibrary2.js',
'script3': 'MyJavascriptLibrary3.js'
},
callback: {
'script1': function (url, result, key) {
console.log('MyJavascriptLibrary1.js loaded'); //will not load in this example
},
'script2': function (url, result, key) {
console.log('MyJavascriptLibrary2.js loaded first');
},
'script3': function (url, result, key) {
console.log('MyJavascriptLibrary3.js loaded second');
}
}
});
If triggering false, MyJavascriptLibrary2.js and MyJavascriptLibrary3.js will load in the appropriate order, no matter what elements influence how they would behave normally (file size, connection speed, etc.). In these callbacks, you may fire additional javascript as well, in the order you wish to do so. Example:
'script2': function (url, result, key) {
alert('anything in here will fire before MyJavascriptLibrary3.js executes');
},
Note this can be done without Modernizr.load({...
but using simply yepnope({...
For more documentation, check out the yepnope.js API