Search code examples
javascriptgoogle-chromegoogle-chrome-extension

How do you identify a specific Frame ID and inject content into its Body?


My goal is to insert a string (append) to existing content in an iFrame through various user actions in my Chrome Extension.

The decision to insert/inject a string will be made by a content.js script running on the page and all its iframes. The code of the CS is below.

Using activeElement doesn't get me the ID of a frame (it does get htmlarea, input).

Here is code from my content.js file that picks up the activeElement ID & tag...

var activeEl = null;
var activeTag = null;
var activeID = null;

document.addEventListener('focusin',function(event){
  console.log('Detected a focusin...')
  activeEl = document.activeElement;
  activeTag = activeEl.tagName.toLowerCase();
  activeID = activeEl.id;
  console.log("focusin and type of element is: " + activeEl.id + " / " + activeTag);
});

background.js sends a message to content.js with the text to insert at the cursor location:

chrome.runtime.onMessage.addListener(
    function(request, sender, sendResponse) {
      console.log("Made it to message receiver in content.js with request: ") + console.log(request);

    if (request.method == 'insertComment'){
      insertAtCursor(request.comment);
    }
    else {
      console.log('Message didn\'t come from insertText...');
    }
});

insertAtCursor processes based on the type of Element. If it's an iFrame I don't have a process to insert the value at the cursor position - I just message the user with an alert and stuff the text into the clipboard:

function insertAtCursor(sValue) {

  console.log('Starting insert routine...');

  var currentEl = document.activeElement.tagName.toLowerCase();      //Checking for currently selected area

//Need to take into account 2 scenarios:
  // 1) Google Docs, where the last Element will be iFrame but we've opened a simple textarea in the form of a bubble that won't show as active (clicked) area
  // 2) HTML editors that have been changed to plain text...which will not have empty objects for cilcked/keypunched area.

  console.log("currentEl before logic: " + currentEl + " / " + document.activeElement.id);
  console.log("activeTag: " + activeTag);


  if (activeTag === undefined || activeTag === null){
    console.log('currentEl in logic: ' + currentEl);
    if (currentEl === 'iframe'){
      activeTag = 'iframe';
      console.log('Making activeTag equal iframe');
    }
  }

  var sField = activeEl;
  /
  if (activeTag === 'input' || activeTag === 'textarea'){
    // console.log('Dealing with plain input/textarea - yes!');

    try {
      var nStart = sField.selectionStart;
      var nEnd = sField.selectionEnd;
    }
      catch (e) {
       //statements to handle any exceptions
       console.log("Can't grab start and end...")
        sField.value = sValue;
    }

     if (nStart || nEnd == '0'){
        // console.log("Inside insert sub with starting point: " + nStart + ' and end ' + nEnd + ' with value ' + sValue);

        sField.value = sField.value.substring(0, nStart) + sValue + sField.value.substring(nEnd, sField.value.length);
        sField.selectionStart = nStart + sValue.length;
        sField.selectionEnd = nStart + sValue.length;
       }
    else {
      sField.value = sValue;
    }
  }

  else if (activeTag === "div"){
    // console.log('We know you are a div...');
    var sel, range;
        if (window.getSelection) {
            // IE9 and non-IE
            sel = window.getSelection();
            if (sel.getRangeAt && sel.rangeCount) {
                range = sel.getRangeAt(0);
                range.deleteContents();

                var el = document.createElement("div");
                el.innerHTML = sValue;
                var frag = document.createDocumentFragment(), node, lastNode;
                while ( (node = el.firstChild) ) {
                    lastNode = frag.appendChild(node);
                }
                var firstNode = frag.firstChild;
                range.insertNode(frag);

                // Preserve the selection
                if (lastNode) {
                    range = range.cloneRange();
                    range.setStartAfter(lastNode);
                    // if (selectPastedContent) {
                        range.setStartBefore(firstNode);
                    // } else {
                    //     range.collapse(true);
                    // }
                    sel.removeAllRanges();
                    sel.addRange(range);
                }
            }
        } else if ( (sel = document.selection) && sel.type != "Control") {
            // IE < 9
            var originalRange = sel.createRange();
            originalRange.collapse(true);
            sel.createRange().pasteHTML(sValue);
            // if (selectPastedContent) {
                range = sel.createRange();
                range.setEndPoint("StartToStart", originalRange);
                range.select();
            }
        }

  else if (activeTag === "iframe" ){

    //try using current activeElement...
    console.log('iFrame, Body, or Button...' + window.location.href);


    if (currentEl === 'iframe') {

      $('#' + activeID).contents().find('div').html(sValue);

      console.log('Not a textarea, input, or editable div - iframe with ID: ' + activeID);
      alert("This message is from Annotate PRO: \n\nWe can't insert text into an this sort of input area (an iFrame) - sorry! \n\nIf you can, change the editor to TEXT ONLY and try again.\n\nAs a workaround, we've copied your selected Comment into the clipboard, so you could just PASTE that bad boy in there right now and be done with it!");
      console.log('Try to use clipboard...');

      const input = document.createElement('input');
      input.style.position = 'fixed';
      input.style.opacity = 0;
      input.value = sValue;
      document.body.appendChild(input);
      input.select();
      document.execCommand('Copy');
      document.body.removeChild(input);

    }   //End check for iFrame

  }   //End iframe

  else if (activeTag === "body" || activeTag === "button"){

    var sField = document.activeElement;
    try {
      var nStart = sField.selectionStart;
      var nEnd = sField.selectionEnd;
    }
      catch (e) {
       // statements to handle any exceptions
      //  console.log("Can't grab start and end...")
        sField.value = sValue;
    }

    if (nStart || nEnd == '0'){
      // console.log("Inside insert sub with starting point: " + nStart + ' and end ' + nEnd + ' with value ' + sValue);

      sField.value = sField.value.substring(0, nStart) + sValue + sField.value.substring(nEnd, sField.value.length);
      sField.selectionStart = nStart + sValue.length;
      sField.selectionEnd = nStart + sValue.length;
     }
    else {
       sField.value = sValue;
     }
    }   //End Else for non-iFrame. Made need to add more conditions here or make a subroutine

  // }


}   //End insertAtCursor

I believe I can use chrome.tabs.executeScript to inject into a specific iFrame, but I'm not sure of syntax or how to get the frameId.

Any help appreciated.

Manifest file (I edited some of the account numbers...not sure how sensitive they are):

{

"manifest_version": 2,

"name": "Annotate PRO for Chrome",
"short_name": "Annotate PRO",
"description": "Right-click access to a pre-written library of comments. Write it once, to perfection, and reuse forever!",
"version": "3.1.0.5",

"permissions": ["identity",
    "identity.email",
    "https://www.googleapis.com/",
    "clipboardWrite",
    "clipboardRead",
    "activeTab",
    "tabs",
    "contextMenus",
    "storage", 
    "webNavigation",
    "*://*/*",
    "http://*/*",
    "https://*/*"],

"content_security_policy": "script-src 'self' https://ssl.google-analytics.com; object-src 'self'",

"externally_connectable": {
    "matches": ["http://*.11trees.com/*"]},

    "commands": {
      "_execute_browser_action": {
        "suggested_key": {
          "windows": "Alt+A",
          "mac": "Alt+A",
          "chromeos": "Alt+A",
          "linux": "Alt+A"
        }
      }
    },

"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjrzouXmcjbWUKDjv5P/YMC0Ar57tk04MS2lSOW1V+SWqvck1iuJmUxW3PXHDrfdsNP2xnAB+wI7Qy9fM7VW95ELgRGcUnynk43WvZ1PtLV/QTTnYhFbIblaJcFmiVo48jpX9V6NaanjfYkpKwUXiM67vmvVNDftGz0wIDAQAB",

"oauth2": {
    "client_id": "4254-smaehlatsj3jmlrrecm.apps.googleusercontent.com",
    "scopes": [
      "https://www.googleapis.com/auth/chromewebstore.readonly"
    ]
  },

"background": {"scripts": ["/dscripts/jquery-3.1.1.min.js","/scripts/background.js", "/scripts/buy.js", "/scripts/contextMenus.js", "/scripts/accountChrome.js"]},


"content_security_policy": "script-src 'self' https://ssl.google-analytics.com; object-src 'self'",


"content_scripts": [
    {
    "all_frames" : true,
    "matches": ["http://*/*","https://*/*"],
    "js": ["/scripts/content.js"]
    }
],

 "icons": {
          "16": "icon.png",
          "48": "icon.png",
          "128": "icon.png"
        },

"browser_action": {
    "default_icon": {
        "19": "icon.png",
        "38": "icon.png"
    },
    "default_title": "Annotate PRO for Google Chrome",
    "default_popup": "popup.html"
}

}

Update With Makyen's help I have the content script successfully messaging background.js:

//Message background.js
  chrome.runtime.sendMessage({type: 'injectTheAdditionalScript'});

I tweaked the provided code to make it work...and I'm able to get the console.log messages showing in the sending page and also see the frameId in the background.js console.log.

chrome.runtime.onMessage.addListener(function (message, sender, sendResponse){
    console.log('In Background and received message from iFrame...' + sender.tab.id + ' / ' + sender.frameId);
    if(typeof message === 'object' && message.type === 'injectTheAdditionalScript') {
        chrome.tabs.executeScript(sender.tab.id,{
          frameId: sender.frameId,
          code: "\
                  console.log('Injecting a console log...');\
                  console.log('2nd line injecting a console log...');\
                  iframe.contentDocument.write('Hello world...');\
                "
        });
        // chrome.tabs.executeScript(sender.tab.id,{
        //     frameId:sender.frameId,
        //     // file:'theAdditionalScript.js'
        //     {code: "console.log('Background.js injecting a script...')";
        // });
    }
});

Now I just have to figure out how to use that frameId to inject more than just console.log into the sending page:)


Solution

  • From our discussion in chat, I was under the impression that you desired to inject an additional script into the frame in which the content script is running. However, it may be that you desire to get some text, or HTML, from your background script.

    In both of these cases, the interaction between the content script and background script is initiated by the content script that is running in the iframe in which you want the data, or the additional script injected. In such cases, you either can get the frameId from the sender of the initial message sent by the content script, or you don't need to know the frameId.

    Injecting a script

    You have stated that the choice to inject an additional script is being made from the content script in the iframe in which you want the additional script to be injected. In that case, you can just runtime.sendMessage() to the background script. Then, the background script can inject the additional script into the frame from which the message was sent. The frameId is part of the data structure provided in the sender.

    To do so, you could so something like:

    Content script:

    //Tell the background script to inject the additional script
    chrome.runtime.sendMessage({type: 'injectTheAdditionalScript'});
    

    Background script:

    chrome.runtime.onMessage(message, sender, sendResponse) {
        if(typeof message === 'object' && message.type === 'injectTheAdditionalScript') {
            chrome.tabs.executeScript(sender.tab.id,{
                frameId:sender.frameId,
                file:'theAdditionalScript.js'
            });
        }
    });
    

    Getting some data (text)

    With the interaction initiated by the content script, if what you desire is some data (e.g. text), then you don't need to know the frameId to send the data back tot he content script, you can just use the sendResponse function.

    Content script:

    //Tell the background script to inject the additional script
    chrome.runtime.sendMessage({type: 'sendTheData'}, function(response){
        doSomethingWithResponseData(response);
    });
    

    Background script:

    chrome.runtime.onMessage(message, sender, sendResponse) {
        if(typeof message === 'object' && message.type === 'sendTheData') {
            sendResponse(theData);
        }
    });