9

I am writing a Chrome extension that needs to prevent webpages from triggering the document visibilitychange event. At the very least I need to be able to overwrite the document.visibilityState (even though it is a read-only property). If not possible, since this extension is for my purposes only and will not be on the Chrome extension store, is there a way I can config my Chrome Browser to achieve what I want? I only need to use this extension while Chrome "Developer Mode" is on, no other time.

I hope someone can think of a creative way to achieve this. Thank you.

Please note! There was a solution in an answer 4 years ago that no longer takes effect in newer versions of Chrome: Spoof or disable the Page Visibility API

Test it out yourself:

// This codes worked 4 years ago but not anymore
var c='(function(){var a=Node.prototype.addEventListener;Node.prototype.addEventListener=function(e){if(e=="visibilitychange"||e=="webkitvisibilitychange"){}else a.apply(this,arguments)}})()'
, E=document.documentElement;
E.setAttribute('onreset', c);
E.dispatchEvent(new CustomEvent('reset'));
E.removeAttribute('onreset');

// THIS WILL STILL LOG THE STATES EVEN WITH THE ABOVE CODE RUNNING
document.addEventListener("visibilitychange", function() {
    console.log( document.visibilityState );
});

If its not possible in Chrome, is there Firefox/Safari/Opera Browser code that can achieve this?

RichardW
  • 249
  • 4
  • 8
  • Didn't test it but the code should be using `EventTarget.prototype`, not Node. – wOxxOm Dec 05 '17 at 20:42
  • Also, the page may forbid inline code so I'd try to insert that `c` text in a `script` element which is excluded from page CSP when add by a Chrome extension's content script. – wOxxOm Dec 05 '17 at 20:44
  • 1
    I tried replacing Node.prototype with EventTarget.prototype and still no luck. The code is in a separate file though called dont.js and I made sure it was running by throwing an alert() in there and I confirmed it was running. – RichardW Dec 05 '17 at 21:30

3 Answers3

21

Here's my solution:

for (event_name of ["visibilitychange", "webkitvisibilitychange", "blur"]) {
  window.addEventListener(event_name, function(event) {
        event.stopImmediatePropagation();
    }, true);
}

I added the blur event because the video I wanted to skip (everfi.net) used it to detect when I switched windows. Blocking that event along with visibilitychange and webkitvisibilitychange did the trick :)

I also modified the extension's manifest so that it works inside iframes.

Full code (chrome extension): https://github.com/NavinF/dont

Confirmed working with the following dog tags:

Google Chrome   63.0.3239.132 (Official Build) (64-bit)
Revision    2e6edcfee630baa3775f37cb11796b1603a64360-refs/branch-heads/3239@{#709}
OS  Mac OS X
JavaScript  V8 6.3.292.49
Command Line    /Applications/Google Chrome.app/Contents/MacOS/Google Chrome --flag-switches-begin --flag-switches-end
Navin
  • 3,681
  • 3
  • 28
  • 52
1

I found a great solution on StackExchange:

Object.defineProperty(document, 'visibilityState', {value: 'visible', writable: true});
Object.defineProperty(document, 'hidden', {value: false, writable: true});
document.dispatchEvent(new Event("visibilitychange"));

In case anyone has trouble getting the above solution or any of the other solutions on this page to work inside their browser extension (like I did), the solution is to add the script that spoofs the visibility API directly to the DOM.

In manifest.json I added the following:

"content_scripts": [
    {
      "matches": ["*://*/*"],
      "js": ["disable.js"]
    }
 ]

And then I used disable.js to add a script that actually manipulates the page.

let s = document.createElement("script");
s.setAttribute("id", "yourScriptID");
s.textContent = `
/**
 * Monitors changes to the page visibility.
 */
document.addEventListener('visibilitychange', () => {
  /**
   * Uncomment this line to debug whether the
   * Page Visibility API is being spoofed.
   * console.log('Document.hidden = "${document.hidden}".');
   **/
    Object.defineProperty(document, 'visibilityState', {
        value: 'visible',
        writable: true,
    });
    Object.defineProperty(document, 'hidden', { value: false, writable: true });
  });`;
(document.head || document.documentElement).appendChild(s);

The reason for doing things this way is that "content scripts live in an isolated world."

What this means is that they have access to the DOM of the page they are injected into, but not to any JavaScript variables or functions created by the page.

You'll find the following discussion very helpful when it comes to understanding how content scripts interact with pages and how to safely/properly inject scripts via your extension:

Access variables and functions defined in page context using a content script

HTH, Mwale

Mwalek
  • 29
  • 3
0

This userscript works well in my case. It's code is relatively compact.

unsafeWindow.onblur = null;
unsafeWindow.blurred = false;
 
unsafeWindow.document.hasFocus = function () {return true;};
unsafeWindow.window.onFocus = function () {return true;};
 
Object.defineProperty(document, "hidden", { value : false});
Object.defineProperty(document, "mozHidden", { value : false});
Object.defineProperty(document, "msHidden", { value : false});
Object.defineProperty(document, "webkitHidden", { value : false});
Object.defineProperty(document, 'visibilityState', { get: function () { return "visible"; } });
 
unsafeWindow.document.onvisibilitychange = undefined;
 
for (event_name of ["visibilitychange",
                    "webkitvisibilitychange",
                    "blur", // may cause issues on some websites
                    "mozvisibilitychange",
                    "msvisibilitychange"]) {
  window.addEventListener(event_name, function(event) {
        event.stopImmediatePropagation();
    }, true);
}

Note that unsafeWindow is only available inside a userscript host extension (like Tampermonkey) and is an unwrapped window object as seen by a page.

nikicat
  • 298
  • 3
  • 10