Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

insert content script when page was changed by history.pushState and ajax call

I've faced with the problem with inserting content script into the page which was changed by history.pushState and ajax call. I've found the similar topic at stackoverflow, but that solution doesn't work for me (that solution was in using of chrome.webNavigation.onHistoryStateUpdated and "popstate" event ).

Here is a fragment of my manifest:

"content_scripts": [
    {
      "matches": ["https://vk.com/audios*", "https://vk.com/al_audio.php*"],
      "js": ["jquery-2.1.4.min.js", "getListOfSongs.js"]
    }
  ]

chrome.webNavigation.onHistoryStateUpdated works only if i navigate to the another page, if i navigate to the same page many times in sequence nothing happens. For example: it works when

1) Go to the https://vk.com/audios* - opening page first time or reloading

2) Go to the https://vk.com/some_other_page - ajax call

3) Go to the https://vk.com/audios* - ajax call

It doesn't work when

1) Go to the https://vk.com/audios* - opening page first time or reloading

2) Again go to the https://vk.com/audios* - ajax call, at this point content script isn't injecting
3) Again go to the https://vk.com/audios* - ajax call, at this point content script isn't injecting and so on

Every time i am clicking to the same page for the second time and so on the following request is generating:

https://vk.com/al_audio.php?__query=audios*********&_ref=left_nav&_smt=audio%3A2&al=-1&al_id=********&_rndVer=60742

(parameters of request may vary)

Also JQuery .ajaxComplete doesn't catch any events in this case.

And pushState doesn't fire "popstate" event, so i can't use window.onpopstate event

I might use chrome.webNavigation.onDOMContentLoaded and chrome.webNavigation.onCompleted but when i reload page these events are happening more then one time, so script will be injected more than one time.

What is the best solution for this case?

like image 229
ivan_ochc Avatar asked Sep 28 '22 12:09

ivan_ochc


1 Answers

There are two possible ways I can think of:

1 - Use timers to check if your script is still there, if not, add again...
2 - Check for ajax calls and if their url matches one of the urls that remove your script, add the script again.

Your script (the one defined in manifest) is still there, even after the ajax calls, it just doesn't run again (not sure what happens with the history pusher). So, I'm assuming you need to just readd some elements or rerun the stript. I supposed you adding the script appending an html tag.

So what you need is something to readd elements or rerun a certain code.


1 - Timer approach - I created a solution for any element (not only scripts) that I wish to add to a certain target element in a page.

It uses a timer to check if the target element is present. When it finds the target element, it adds mine. Then the timer is adjusted to check if my element is still there. If not, add again.

You just need to call appendChildPersistent a single time and this will keep active all the time you navigate around.

var timers = {}; //stores the setInterval ids

//this is the only method you need to call
//give your script an `id` (1)
//the child is your script, it can be anything JQuery.append can take
//toElem is the Jquery "SELECTOR" of the element to add your script into.
//I'm not sure what would happen if toElem were not a string.
//callback is a function to call after insertion if desired, optional.
appendChildPersistent = function(id, child, toElem, callback)
{
    //wait for target element to appear
    withLateElement(toElem, function(target)
    {
        target.append(child); //appends the element - your script                                                                                                           
        if (typeof callback !== 'undefined') callback(); //execute callback if any

        //create a timer to constantly check if your script is still there
        timers[id] = setInterval(function()
        {                                       
            //if your script is not found, clear this timer and tries to add again          
            if (document.getElementById(id) === null)
            {
                clearInterval(timers[id]);
                delete timers[id];
                appendChildPersistent(id, child, toElem, callback);
            }
        },3000);

    });
}

//this function waits for an element to appear on the page
//since you can't foresee when an ajax call will finish
//selector is the jquery selector of the target element
//doAction is what to do when the element is found
function withLateElement(selector, doAction)
{   
    //checks to see if this element is already being waited for                             
    if (!(selector in timers))
    {
        //create a timer to check if the target element appeared                                                            
        timers[selector] = setInterval(function(){              
            var elem = $(selector);

            //checks if the element exists and is not undefined
            if (elem.length >= 0)
            {
                if (typeof elem[0] !== 'undefined')
                {
                    //stops searching for it and executes the action specified
                    clearInterval(timers[selector]);
                    delete timers[selector];
                    doAction(elem);
                }
            }
        }, 2000);
    }                                                           
}

(1) It seems it's not a problem to add an Id to a script tag: Giving the script tag an ID


2 - Capture the ajax calls

An option is to use chrome.webRequest. But strangely, this didn't work for me. Another option is below.

For this case, check this answer, and don't forget to read the related answer to Chrome extension in there. It will only work if you follow the entire procedure. Fortunately, I tested it today and it works great :p

Here, what you do is to change the XMLHttpRequest methods open and send to detect (and possibly get the parameters too) when they are called.

In the Google Extension, however, it's absolutely necessary that you inject the stript in the page (not a background page or script injecting your content script, but your content script injecting some code into the dom, like the following).

var script = document.createElement('script');
script.textContent = actualCode; //actual code is the code you want to inject, the one that replaces the ajax methods
document.head.appendChild(script); //make sure document.head is already loaded before doing it
script.parentNode.removeChild(script); //I'm not sure why the original answer linked removes the script after that, but I kept doing it in my solution

This is crucial because the extension tries to create an isolated environment, and the changes you do to the XMLHttpRequest in this environment will simply not take part. (That's why JQuery.ajaxComplete doesn't seem to work, you need to inject a script in the page for it to work - look here)

In this pure javascript solution, you replace the methods:

//enclosing the function in parentheses to avoid conflict with vars from the page scope
(function() {
    var XHR = XMLHttpRequest.prototype;

    // Store the orignal methods from the request
    var open = XHR.open;
    var send = XHR.send;

    // Create your own methods to replace those

    //this custom open stores the method requested (get or post) and the url of the request
    XHR.open = function(method, url) {
        this._method = method; //this field was invented here
        this._url = url; //this field was invented here
        return open.apply(this, arguments); //calls the original method without any change

        //what I did here was only to capture the method and the url information
    };


    //this custom send adds an event listener that fires whenever a request is complete/loaded
    XHR.send = function(postData) {
        //add event listener that fires when request loads
        this.addEventListener('load', function() {
            //what you want to do when a request is finished
            //check if your element is there and readd it if necessary
            //if you know the exact request url, you can put an if here, but it's not necessary

            addMyElementsToPage(); //your custom function to add elements
            console.log("The method called in this request was: " + this._method);
            console.log("The url of this request was: " + this._url);
            console.log("The data retrieved is: " + this.responseText);

        });

        //call the original send method without any change
        //so the page can continue it's execution
        return send.apply(this, arguments);

        //what we did here was to insert an interceptor of the success of a request and let the request continue normally
    };
})();
like image 154
Daniel Möller Avatar answered Nov 01 '22 17:11

Daniel Möller