11

I want to restrict the browser to within a set of URLs. I'm using:

chrome.webNavigation.onBeforeNavigate.addListener(functon(details){
    if (notAllowed(details.url)) {
         // Do something to stop navigation
    }
});

I know that I can cancel chrome.webRequest.onBeforeRequest. But, I don't want to block requests, like XHR or any other. I want this filter to be applied only for navigation.

For the user, it should looks like, the link (e.g. <a href="http://...">foo</a>) click event was stopped.

Makyen
  • 27,758
  • 11
  • 68
  • 106
dkiselev
  • 750
  • 7
  • 19
  • 1
    Hmm. I guess you can inject `window.stop()` as shown [here](https://stackoverflow.com/q/4532236) – wOxxOm Sep 05 '16 at 16:33
  • You tell us that you are using it, but don't *explicitly* say what *exactly* it is doing which you don't desire. In addition, we need a specific case where it is not doing what you desire (e.g. a website, and the user action within that website which caused what you don't want). In other words, we need **all of** a description of *exactly* what you want to happen, a description of *exactly* what it is doing that you don't desire *and* a **complete** [mcve] (along with the test case, i.e. a website) which we can use to duplicate the problem you are having. – Makyen Sep 05 '16 at 16:34
  • 2
    @wOxxOm, it seems that events are processed asynchromusly, so it's possible that I'll stop the window in wrong state. Especially for pages which uses the redirects, like google search results. – dkiselev Sep 05 '16 at 16:43
  • Yes, chrome.* API is async but I don't see why it could be a problem here. Anyway, there are no other methods I can think of even theoretically. – wOxxOm Sep 05 '16 at 16:46
  • My mistake, it was not clear to me that what you are really asking is: "From an `onBeforeNavigate` event handler, how do I prevent/cancel the navigation to the new URL?" – Makyen Sep 05 '16 at 16:46
  • Try: use `webNavigation.onCommitted`/`webNavigation.onCompleted` to keep a list of the current URLs for each tab. If you get a `webNavigation.onBeforeNavigate` event to a URL which is blocked, just cause the affected tab to retain (navigate to) the currently visible URL. This should end up effectively canceling the current navigation to the blocked URL. I'll work up a test case/example later today and post an answer if it works. I have some other things I need to do right at the moment. – Makyen Sep 05 '16 at 16:56
  • The thing to look out for is whether re-navigating to current URL may reload the page in some cases, losing the state/data/scroll position. – wOxxOm Sep 05 '16 at 17:04
  • @wOxxOm, I expect that would be possible (I would want to try it to see what it looks like to the user in multiple situations). But, I was under the impression that the more important thing was to prevent navigation to the blocked URL. IMO, blocking should come with a notification to the user that the navigation was blocked, unless there has been a configuration option specifically set that blocking is to be silent. – Makyen Sep 05 '16 at 17:11
  • @wOxxOm, You are correct, the current URL is reloaded. A better solution would be to prevent navigation prior to beginning the navigation. However, there are multiple ways which the navigation could have been started. All of which would need to be covered (or leave the `webNavigation.onBeforeNavigate` as a backup for any that were missed). – Makyen Sep 05 '16 at 20:35

2 Answers2

4

The following extension adds a listener to webNavigation.onCompleted which is used to remember, indexed by tabId, both the most recent URL in frameId==0 for which the event is fired, and the prior URL.

A listener is added to webNavigation.onBeforeNavigate which watches for matching URLs, in this case, stackexchange.com. If the URL matches, the tab URL is updated, via tabs.update, to navigate back to the last URL for which a webNavigation.onCompleted event was fired.

If the onBeforeNavigate event is for a frameId other than 0, then the tab is navigated to the previous URL for which a onCompleted event was fired for frameId==0. If the prior URL was not used, then we could get into a loop where the current URL is repeatedly re-loaded due to the URL in one of its frames matching the URL we are blocking. A better way to handle this would be to inject a content script to change the src attribute for the frame. We would then need to handle frames within frames.

blockNavigation.js:

//Remember tab URLs
var tabsInfo = {};
function completedLoadingUrlInTab(details) {
    //console.log('details:',details);
    //We have completed loading a URL.
    createTabRecordIfNeeded(details.tabId);
    if(details.frameId !== 0){
        //Only record inforamtion for the main frame
        return;
    }
    //Remember the newUrl so we can check against it the next time
    //  an event is fired.
    tabsInfo[details.tabId].priorCompleteUrl = tabsInfo[details.tabId].completeUrl;
    tabsInfo[details.tabId].completeUrl = details.url;
}

function InfoForTab(_url,_priorUrl) {
    this.completeUrl = (typeof _url !== 'string') ? "" : _url;
    this.priorCompleteUrl = (typeof _priorUrl !== 'string') ? "" : _priorUrl;
}

function createTabRecordIfNeeded(tabId) {
    if(!tabsInfo.hasOwnProperty(tabId) || typeof tabsInfo[tabId] !== 'object') {
        //This is the first time we have encountered this tab.
        //Create an object to hold the collected info for the tab.
        tabsInfo[tabId] = new InfoForTab();
    }
}


//Block URLs
function blockUrlIfMatch(details){
    createTabRecordIfNeeded(details.tabId);
    if(/^[^:/]+:\/\/[^/]*stackexchange\.[^/.]+\//.test(details.url)){
        //Block this URL by navigating to the already current URL
        console.log('Blocking URL:',details.url);
        console.log('Returning to URL:',tabsInfo[details.tabId].completeUrl);
        if(details.frameId !==0){
            //This navigation is in a subframe. We currently handle that  by
            //  navigating to the page prior to the current one.
            //  Probably should handle this by changing the src of the frame.
            //  This would require injecting a content script to change the src.
            //  Would also need to handle frames within frames. 
            //Must navigate to priorCmpleteUrl as we can not load the current one.
            tabsInfo[details.tabId].completeUrl = tabsInfo[details.tabId].priorCompleteUrl;
        }
        var urlToUse = tabsInfo[details.tabId].completeUrl;
        urlToUse = (typeof urlToUse === 'string') ? urlToUse : '';
        chrome.tabs.update(details.tabId,{url: urlToUse},function(tab){
            if(chrome.runtime.lastError){
                if(chrome.runtime.lastError.message.indexOf('No tab with id:') > -1){
                    //Chrome is probably loading a page in a tab which it is expecting to
                    //  swap out with a current tab.  Need to decide how to handle this
                    //  case.
                    //For now just output the error message
                    console.log('Error:',chrome.runtime.lastError.message)
                } else {
                    console.log('Error:',chrome.runtime.lastError.message)
                }
            }
        });
        //Notify the user URL was blocked.
        notifyOfBlockedUrl(details.url);
    }
}

function notifyOfBlockedUrl(url){
    //This will fail if you have not provided an icon.
    chrome.notifications.create({
        type: 'basic',
        iconUrl: 'blockedUrl.png',
        title:'Blocked URL',
        message:url
    });
}


//Startup
chrome.webNavigation.onCompleted.addListener(completedLoadingUrlInTab);
chrome.webNavigation.onBeforeNavigate.addListener(blockUrlIfMatch);

//Get the URLs for all current tabs when add-on is loaded.
//Block any currently matching URLs.  Does not check for URLs in frames.
chrome.tabs.query({},tabs => {
    tabs.forEach(tab => {
        createTabRecordIfNeeded(tab.id);
        tabsInfo[tab.id].completeUrl = tab.url;
        blockUrlIfMatch({
            tabId : tab.id,
            frameId : 1, //use 1. This will result in going to '' at this time.
            url : tab.url
        });

    });
});

manifest.json:

{
    "description": "Watch webNavigation events and block matching URLs",
    "manifest_version": 2,
    "name": "webNavigation based block navigation to matched URLs",
    "version": "0.1",
    "permissions": [
        "notifications",
        "webNavigation",
        "tabs"
    ],
    "background": {
        "scripts": ["blockNavigation.js"]
    }
}
Makyen
  • 27,758
  • 11
  • 68
  • 106
  • 2
    Hi Makyen it's the closest approach to what I want to have, except that it still will cause a state reset for js one-page applications. – dkiselev Sep 13 '16 at 18:01
  • 1
    @dkiselev, There are other possible approaches. The "correct" way is with a content script that usurps `click` events, preventing any which would result in navigation off the page. While you could not get all ways of navigating, you would get most. You would still need the above as backup to prevent any navigation you did not catch by other methods. I did not explore other methods in this answer as your question explicitly asks about acting within a `onBeforeNavigate` handler. For approaches to usurping a `click` event, you can see [this answer](http://stackoverflow.com/a/39431950/3773011). – Makyen Sep 13 '16 at 18:28
  • In 2020, it seems to be possible to do universally, including top-bar navigation. See my answer below. – Maciej Krawczyk Jun 28 '20 at 16:18
  • that won't work , because the redirection happen after the page was already loaded ( so it's not blocking ) – Zack Heisenberg Sep 09 '20 at 02:45
  • using chrome.webRequest.onBeforeRequest is much better because it's allow you to Block the request then redirect it – Zack Heisenberg Sep 09 '20 at 02:46
2

It is possible to prevent navigation altogether. Use redirectURL and set a link which generates 204 (no content) response.

chrome.webRequest.onBeforeRequest.addListener(

  function(details) {

    //just don't navigate at all if the requested url is example.com
    if (details.url.indexOf("://example.com/") != -1) {

      return {redirectUrl: 'http://google.com/gen_204'};

    } else {

      return { cancel: false };

    }

  },
    { urls: ["<all_urls>"] },
    ["blocking"]
  );
Maciej Krawczyk
  • 10,012
  • 5
  • 30
  • 38
  • Update: Please note that this might not be future proof. There will be some changes to the API if/when the limited declarative web request will replace the original API. – Maciej Krawczyk Sep 26 '20 at 05:15