Chrome extension - How to change text in page frames - google-chrome-extension

I want to write a chrome extension that changes a text on all frames on a webpage. I wrote the following code, but with it, I can only change the text on regular pages, not frames. Any idea how to do it?
var elements = document.getElementsByTagName('*');
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
for (var j = 0; j < element.childNodes.length; j++) {
var node = element.childNodes[j];
if (node.nodeType === 3) {
var text = node.nodeValue;
var replacedText = text.replace('Hello', 'Hello2');
if (replacedText !== text) {
element.replaceChild(document.createTextNode(replacedText), node);
}
}
}
}

Set "all_frames": true in your manifest.json for content-script. (see declaration).
Example:
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["http://www.google.com/*"],
"js": ["content-script.js"],
"all_frames": true
}
],
...
}

Related

insert text at cursor position in a search field

This is what I would like to do: copy some text in the clipboard. Place the cursor in a search field in the browser page, call the context menu of my extension which will bring a regular expression.
Apply the regex to clipboard content, put the result in the search field.
If I fetch the cplipboard content with:
navigator.clipboard.readText().then((clipText)=>
{
let val =applyRegex(clipText, data);
insertTextAtCursor(val);
}
InsertTextAtCursor below works, at least on google search page.
function insertTextAtCursor(text) {
log("insertTextAtCursor: " + text)
let el = document.activeElement;
let val = el.value;
let endIndex;
let range;
let doc = el.ownerDocument;
if (typeof el.selectionStart === 'number' &&
typeof el.selectionEnd === 'number') {
endIndex = el.selectionEnd;
el.value = val.slice(0, endIndex) + text + val.slice(endIndex);
el.selectionStart = el.selectionEnd = endIndex + text.length;
// && doc.selection.createRange)
} else if (doc.selection !== 'undefined' && doc.selection.createRange) {
el.focus();
range = doc.selection.createRange();
range.collapse(false);
range.text = text;
range.select();
//document.execCommand('insertText', false, text);
}
}//insertTextAtCursor
If I fetch the clipboard content with
function paste() {
var input = document.createElement('textarea');
//input.value = text;
//input.setAttribute('readonly', '');
input.style.position= 'absolute';
input.style.left= '-9999px';
document.body.appendChild(input);
input.select();
var result = document.execCommand('paste');
result = input.value;
log("paste: " + result);
document.body.removeChild(input);
return result;
}
doc.selection.createRange crash in InsertTextAtCursor, saying doc.selection is not defined.
I would prefer to stick with document.execCommand('paste'). So any help is welcome !
See https://sourceforge.net/p/skimtheclipboard-mv3/mercurial/ci/default/tree/
for the whole code.
Here is a test for a minimal example.
manifest.json
{
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [ {
"js": [ "contentok.js" ],
"matches": [ "\u003Call_urls>" ]
} ],
"description": "test",
"manifest_version": 3,
"name": "test",
"optional_permissions": [ ],
"permissions": ["contextMenus", "activeTab", "clipboardRead", "clipboardWrite" ],
"version": "0.0.1"
}
background.js
function buildContextMenu () {
let fcb =[ {fcb_context: "fcb_copy", title: "copy", context: ["selection", "link"]},
{fcb_context:"fcb_paste", context:["editable"], title:"paste"}];
for (let i=0; i< fcb.length; i++) {
let menu = fcb[i];
chrome.contextMenus.create({
title: menu.title,
id: menu.fcb_context,
contexts: menu.context,
});
} // for i
} //buildContextMenu
function log(msg, force=false) {
var debug = true;
if (force || debug){console.log(msg);}
}//Log
chrome.runtime.onInstalled.addListener( function () {
log("onInstalled called");
chrome.contextMenus.removeAll();
buildContextMenu();
}); //add listener
chrome.contextMenus.onClicked.addListener(function(info, tabs){
let data, sel, fcb_context;
let id = info.menuItemId;
log(" id: " + id ) ;
sel = info.selectionText;
fcb_context = info.parentMenuItemId;
( async () =>{
const [tab] = await chrome.tabs.query({active: true, currentWindow: true});
const response = await chrome.tabs.sendMessage( tab.id, {data: "go", context: fcb_context, sel: sel}, function(){} );
log(response);
})();
}); //onClickedadd
chrome.runtime.onStartup.addListener(function() {
log("onStartup called");
chrome.contextMenus.removeAll();
buildContextMenu();
}); //addListerner
contentok.js
chrome.runtime.onMessage.addListener(
function(request, sender) {
console.log("received in cs: " + JSON.stringify(request));
navigator.clipboard.readText().then((clipText)=>
{
let val = "Clipboard content : " + clipText;
insertTextAtCursor(val);
}
)
return true;
}
);
function insertTextAtCursor(text) {
console.log("insertTextAtCursor: " + text)
let el = document.activeElement;
let val = el.value;
let endIndex;
let range;
let doc = el.ownerDocument;
if (typeof el.selectionStart === 'number' &&
typeof el.selectionEnd === 'number') {
endIndex = el.selectionEnd;
el.value = val.slice(0, endIndex) + text + val.slice(endIndex);
el.selectionStart = el.selectionEnd = endIndex + text.length;
// && doc.selection.createRange)
} else if (doc.selection !== 'undefined' && doc.selection.createRange) {
el.focus();
range = doc.selection.createRange();
range.collapse(false);
range.text = text;
range.select();
// document.execCommand('insertText', false, text);
}
}//insertTextAtCursor
Once you have copied a string in the clipboard, you go to the google search page (or any page that have an editable field), place the cursor in that field, click on the paste menu of the context menu. This works as expected and inserts the clipboard content in the search field of the page.
Now you change the manifest to have the following content file used:
contentnotok.js which use the function paste()
chrome.runtime.onMessage.addListener(
function(request, sender) {
console.log("received in cs: " + JSON.stringify(request));
let val = paste();
insertTextAtCursor("Clipboard content: " + val);
return true;
}
);
function insertTextAtCursor(text) {
console.log("insertTextAtCursor: " + text)
let el = document.activeElement;
let val = el.value;
let endIndex;
let range;
let doc = el.ownerDocument;
if (typeof el.selectionStart === 'number' &&
typeof el.selectionEnd === 'number') {
endIndex = el.selectionEnd;
el.value = val.slice(0, endIndex) + text + val.slice(endIndex);
el.selectionStart = el.selectionEnd = endIndex + text.length;
// && doc.selection.createRange)
} else if (doc.selection !== 'undefined' && doc.selection.createRange) {
el.focus();
range = doc.selection.createRange();
range.collapse(false);
range.text = text;
range.select();
// document.execCommand('insertText', false, text);
}
}//insertTextAtCursor
function paste() {
var input = document.createElement('textarea');
//input.value = text;
//input.setAttribute('readonly', '');
input.style.position= 'absolute';
input.style.left= '-9999px';
document.body.appendChild(input);
input.select();
var result = document.execCommand('paste');
result = input.value;
console.log("paste: " + result);
document.body.removeChild(input);
return result;
}
If you inspect the page when calling the paste entry in the context menu, you have the Error in event handler: TypeError: Cannot read properties of undefined (reading 'createRange') in line 27 of the script.
As I said I would better use this paste function, which works looking at the log, but calling it seems to prevent insertTextAtCursor from working correctly.
If someone can give a correction ...
François
The textarea of the web page where the user calls the context menu loose the focus because of the paste function which create a new editable element.
So if I store it before calling paste and put the focus again on it after the calls return, insertTextAtCursor works.
In contentnotok.js :
chrome.runtime.onMessage.addListener(
function(request, sender) {
console.log("received in cs: " + JSON.stringify(request));
let field = document.activeElement;
let val = paste();
field.focus();
insertTextAtCursor("Clipboard content : " + val);
return true;
}
);
``

Node.js for loop output only last item from JSON

The code below only output the last result, I don't get it. I check if the updateDate item contains 2020-05 both items does and I get only the last one. The loop is not looping :)
const briefing = [
{
"updateDate": "2020-05-05T00:00:00.0Z",
},
{
"updateDate": "2020-05-06T00:00:00.0Z",
},
{
"updateDate": "2020-05-13T00:00:00.0Z",
}
];
let date = new Date();
var formattedYearMonth = date.getFullYear() + '-' + ('0' + (date.getMonth()+1)).slice(-2) + '-';
for (var i = 0; i < briefing.length; i++) {
var jsonDate = briefing[i].updateDate;
if (jsonDate.includes(formattedYearMonth)) {
var response = JSON.stringify(briefing[i]);
}
}return response;
}
for (var i = 0; i < briefing.length; i++) {
var jsonDate = briefing[i].updateDate;
if (jsonDate.includes(formattedYearMonth)) {
var response = JSON.stringify(briefing[i]); // <==== THIS IS WHERE YOUR PROBLEM LIES
}
}return response;
The loop is actually looping :). But for every run of the loop, you are resetting the value of response.
--EDITED--
For the response to be an array, you need to modify your code as
let response = [];
for (var i = 0; i < briefing.length; i++) {
var jsonDate = briefing[i].updateDate;
if (jsonDate.includes(formattedYearMonth)) {
response.push(JSON.stringify(briefing[i]));
}
}
return response;

parse dirty object is not working after migration

I have a question regarding parse cloud code. The following cloud code was working before migration written in cloud code but after migration its not returning desired output.
var streamClass = Parse.Object.extend("Streams");
streamObj = new streamClass({
objectId: "dummy",
streamerId: usersArr[i]
});
streamObj.dirty = function() {return false;};
There are two entities i.e. streams and users. Every user has streams. So there is users pointer(streamerId) in stream table. If user do not have any stream created then i am creating a stream dummy object and setting user(streamerId) as a pointer in stream object. When this code was called as a API, it was returning stream dummy object with user(streamerId) information before parse server migration. After migration the above code gives the following output.
{
"result": [
{
"__type": "Pointer",
"className": "Streams",
"objectId": "dummy"
}
]
}
It can noticed that there is no user(streamerId) information in the output. Can anyone please help me in this regard.
I am not saving this streamObj. I am returning this streamObj to IOS app. I also tested it through postman in google chrome. The following is a complete function which takes array of users object and array of streams objects and return one object contains user and its related streams.
function getUsersAndRecentStreams(usersArr, streamsArr) {
var responseObj = [];
var moment = require('moment');
var now = moment();
var currentDate = new Date();
for( var i=0; i<usersArr.length; i++ ) {
var streamObj = null;
for( j=0; j<streamsArr.length; j++ ) {
var streamerObj = streamsArr[j].get('streamerId');
if( streamerObj.id === usersArr[i].id ) {
if( moment(streamsArr[j].get('showTimeStart')) <= now && moment(streamsArr[j].get('showTimeEnd')) >= now ) {
streamObj = streamsArr[j];
break;
}
if( streamObj == null) {
streamObj = streamsArr[j];
}
else {
if( moment(streamsArr[j].get('showTimeStart')) <= now ) {
streamObj = streamsArr[j];
}
}
}
}
if( streamObj == null ) {
var streamClass = Parse.Object.extend("Streams");
streamObj = new streamClass({
objectId: "dummy",
streamerId: usersArr[i]
});
streamObj.dirty = function() {return false;};
var streamObj = new streamObj();
}
responseObj.push(streamObj);
}
return responseObj;
}
There are two cases.
1) When streamObj is not null. In this case the output is correct.
2) The second case when streamObj is null. In this case the following output is return which is not desired.
{
"result": [
{
"__type": "Pointer",
"className": "Streams",
"objectId": "dummy"
}
]
}
When streamObj is null, The following desired output should return this function.
{
"result": [
{
"__type": "Pointer",
"className": "Streams",
"objectId": "dummy",
"StreamerId": userObject
}
]
}

Code inject via Content Script gets removed

I am creating a Chrome Extension that would draw an image above tag showing Flash content. I have written the content script in my "contentscript.js" file which does nothing but simply inserts another script file "backgroundscript.js" into page's body via a script tag.
The code works on few of sites having flash videos and shows the image right above the Flash Player window but on other sites it does not show up. Also on few it shows up for a fraction of a second and then gets disappeared.
Below is my manifest file:
{
"name": "My First Chrome Extension",
"version": "1.0.0.0",
"description": "Tag videos from websites with a single click!",
"icons": { "16":"application_16.png","48":"application_48.png","128":"application_128.png" },
"homepage_url": "http://www.myurl.com",
"page_action":{
"default_icon":{
"19":"application_19.png",
"38":"application_38.png"
},
"default_title":"VDownloader browser plugin"
},
"content_scripts": [ { "js": [ "contentscript.js" ], "matches": [ "http://*/*" ], "run_at" : "document_end", "all_frames": true} ],
"plugins": [ { "path": "myplugin.dll", "public": true } ],
"manifest_version":2,
"minimum_chrome_version":"17",
"offline_enabled":true,
"permissions":["tabs","http://*/*", "https://*/*","contextMenus"],
"web_accessible_resources":["application.png","backgroundscript.js"]
}
Also I checked by inserting "debugger;" into my script and monitoring code execution via console; the script gets added at least once into the page body. Even in the cases where no image button is shown.
Is this some kind of anit-XSS control implemented on the the more advanced sites not showing my button?
I would really appreciate your help as I have been trying to get over this from past couple of weeks without success :(
EDIT:
Please see the content script below:
var s = document.createElement('script');
s.src = chrome.extension.getURL("backgroundscript.js");
s.onload = function() {
this.parentNode.removeChild(this);
};
(document.head||document.documentElement).appendChild(s);
Target sites:
Does not show the button: http://www.metacafe.com
Shows the button for a moment: http://www.dailymotion.com
Shows the button correctly: http://www.myreviewalarm.com
Please take a look at backgroundscript.js content:
var vdDelayTimer;
var vdUpdateInterval;
var playerButtons;
embedPlugin();
function embedPlugin() {
var embed = document.createElement('embed');
embed.setAttribute('type', 'application/x-myplugin');
embed.setAttribute('id', 'myplugin');
embed.setAttribute('style', 'width:1px;height:1px;position:absolute;left:0px;top:0px;');
if(document.body!=null)
{
document.body.appendChild(embed);
}
testPlugin();
}
function testPlugin() {
var plugin = document.getElementById('vdPlugin');
if (plugin != null) {
playerButtons = [];
vdDelayTimer = setTimeout("appendButtons()", 2000);
}
else {
window.alert('Plugin does not exist!');
}
}
function updateButtons() {
for (i = 0; i < playerButtons.length; i += 2) {
updateButtonLocation(playerButtons[i], playerButtons[i + 1]);
}
}
function updateButtonLocation(player, button) {
var playerX = getX(player);
var playerY = getY(player);
var buttonHeight = 38;
var buttonWidth = 196;
var buttonX = playerX + player.offsetWidth - buttonWidth - 12;
var buttonY = playerY - buttonHeight + 1;
button.setAttribute('style', 'background-image: url(http://myurl.com/img/browser-download-button.png); width:' + buttonWidth + 'px;height:' + buttonHeight + 'px;position:absolute;left:' + buttonX + 'px;top:' + buttonY + 'px; cursor:pointer; cursor:hand;');
}
function appendButtons() {
debugger;
var objectTags = document.getElementsByTagName('object');
for (var i = 0; i < objectTags.length; i++) {
var objectTag = objectTags[i];
if ((objectTag.hasAttribute('classid') && objectTag.getAttribute('classid') == 'clsid:D27CDB6E-AE6D-11cf-96B8-444553540000')
|| (objectTag.hasAttribute('type') && objectTag.getAttribute('type') == 'application/x-shockwave-flash')) {
if (objectTag.offsetWidth >= 320 && objectTag.offsetHeight >= 240)
createButton(objectTag, getX(objectTag), getY(objectTag), objectTag.offsetWidth);
}
}
var embedTags = document.getElementsByTagName('embed');
for (var i = 0; i < embedTags.length; i++) {
try {
var embedTag = embedTags[i];
if (embedTag.hasAttribute('type') && embedTag.getAttribute('type') == 'application/x-shockwave-flash') {
if (embedTag.offsetWidth >= 320 && embedTag.offsetHeight >= 240)
createButton(embedTag, getX(embedTag), getY(embedTag), embedTag.offsetWidth);
}
}
catch (err) { }
}
var videoTags = document.getElementsByTagName('video');
for (var i = 0; i < videoTags.length; i++) {
try {
var videoTag = videoTags[i];
if (videoTag.hasAttribute('src')) {
if (videoTag.offsetWidth >= 320 && videoTag.offsetHeight >= 240)
createButton(videoTag, getX(videoTag), getY(videoTag), videoTag.offsetWidth);
}
}
catch (err) { }
}
vdUpdateInterval = setInterval("updateButtons();", 500);
}
function createButton(videoTag, playerX, playerY, playerWidth) {
debugger;
var buttonHeight = 38;
var buttonWidth = 196;
var buttonX = playerX + playerWidth - buttonWidth - 12;
var buttonY = playerY - buttonHeight + 1;
var vdPlugin = document.getElementById('vdPlugin');
var strLocation = window.location.toString();
// if (isSupported(strLocation)) {
var downloadButton = document.createElement('img');
downloadButton.setAttribute('title', 'Tag this video');
downloadButton.setAttribute('src', 'http://myurl.com/img/browser-download-button.png');
downloadButton.setAttribute('style', 'background-image: url(http://myurl.com/img/browser-download-button.png); width:' + buttonWidth + 'px;height:' + buttonHeight + 'px;position:absolute;left:' + buttonX + 'px;top:' + buttonY + 'px; cursor:pointer;cursor:hand;z-index:2147483647;');
downloadButton.setAttribute('onclick', 'javascript:document.getElementById(\'vdPlugin\').downloadNow(window.location.href);');
downloadButton.setAttribute('oncontextmenu', 'javascript:return false;');
document.body.appendChild(downloadButton);
playerButtons.push(videoTag, downloadButton);
// }
}
function getY(oElement) {
var iReturnValue = 0;
while (oElement != null) {
iReturnValue += oElement.offsetTop;
oElement = oElement.offsetParent;
}
return iReturnValue;
}
function getX(oElement) {
var iReturnValue = 0;
while (oElement != null) {
iReturnValue += oElement.offsetLeft;
oElement = oElement.offsetParent;
}
return iReturnValue;
}
function isSupported(url) {
var regLen = supportedSites.length;
var regEx;
var res = false;
try {
for (var i = 0; i < regLen && res == false; i++) {
regEx = new RegExp(supportedSites[i], "i");
res = regEx.test(url);
}
}
catch (ex) {
}
return res;
}

Local storage data not returned to content script from backgound.js

In background.js, I store some data into local storage:
localStorage["domain"] = site; //Save the site to local storage for retrieval later when requested by the content script
localStorage["page"] = tab.title; //Save the page title to local storage for retrieval later
localStorage["url"] = tab.url; //Save the URL of the current page to local storage for retrieval
Later, my content script requests the data with
chrome.extension.sendRequest({name:"domain"},
function(response)
{
subjectStr = response.domain;
});
chrome.extension.sendRequest({name:"url"},
function(response)
{
bodyStr = "URL of last page visited: " + response.url;
});
and background.js responds with
//Wait for request for the site value and URL from content script
chrome.extension.onRequest.addListener(
function(request, sender, sendResponse)
{
if (request.name == "url")
{
sendResponse({url: localStorage["url"]});
}
else
{
sendResponse({domain: localStorage["domain"] + ": " + localStorage["page"]});
}
}
);
However, the data is never received by the content script. Anyone see why?
Here's the manifest:
{
"name": "Test",
"version": "1.0",
"manifest_version": 2,
"description": "Test extension",
"browser_action": {
"default_icon": "no_msgs.png",
"default_title": "Press here to test."
},
"background": {
"scripts": ["background.js"]
},
"content_scripts": [{
"run_at": "document_end",
"js": ["postMsg.js"],
"matches": ["https://groups.google.com/forum/*"]
}],
"permissions": ["tabs",
"http://groups.google.com/forum/?fromgroups=#!forum/opencomments-site-discussions/*",
"https://groups.google.com/forum/?fromgroups=#!forum/opencomments-site-discussions/*"
]
}
and no_msgs.png:
no_msgs http://www.opencomments.com/no_msgs.png
and background.js:
var post_url = "https://groups.google.com/forum/?fromgroups=#!newtopic/opencomments-site-discussions";
chrome.browserAction.onClicked.addListener(function(main) {
});
function createEvent(tab){
}
function updateEvent(tabId, changeInfo, tab){
}
function miscEvent(tabId, eventInfo){
}
function getURL() {
chrome.tabs.getSelected(undefined, function(tab) {
var tmp = tab.url;
var site;
if (tab.url.indexOf("http://") == 0 || tab.url.indexOf("https://") == 0) {
site = getDomain(tab.url);
chrome.tabs.create({url: post_url});
localStorage["domain"] = site; //Save the site to local storage for retrieval later when requested by the content script
localStorage["page"] = tab.title; //Save the page title to local storage for retrieval later
localStorage["url"] = tab.url; //Save the URL of the current page to local storage for retrieval
}
});
}
//Wait for request for the site value and URL from content script
chrome.extension.onRequest.addListener(
function(request, sender, sendResponse)
{
if (request.name == "url")
{
sendResponse({url: localStorage["url"]});
}
else
{
sendResponse({domain: localStorage["domain"] + ": " + localStorage["page"]});
}
}
);
//Fetches the domain from the URL
function getDomain(url){
var tmp = url.substring(url.indexOf("//") + 2);
var tmp2 = tmp.indexOf("/");
var str = tmp.substring(0, tmp2);
var index = str.indexOf(".");
while ((tmp = str.substring(index + 1)).indexOf(".") != -1){
str = str.substring(index + 1);
index = str.indexOf(".");
}
index = str.indexOf(".");
return str;
}
// Called when the user clicks on the browser action.
chrome.browserAction.onClicked.addListener(function(tab) {
getURL();
});
and finally postMsg.js:
var subjectStr = '';
var bodyStr = '';
chrome.extension.sendRequest({name:"domain"},
function(response) {
subjectStr = response.domain;
});
chrome.extension.sendRequest({name:"url"},
function(response) {
bodyStr = "URL of last page visited: " + response.url;
});
Works fine with your code, i used messages instead of requests.
Follow Rob W posts(link1, link2) for more information on sendMessage() vs sendRequest()
SAMPLE CODE AND OUTPUT
Output from background.js
Output from Content Script scripts.js
manifest.json
{
"name":"Local Storage",
"description":"Local Storage Demo",
"manifest_version":2,
"background":{
"scripts":["background.js"]
},
"content_scripts": [
{
"matches": ["https://www.google.co.in/*"],
"js": ["scripts.js"]
}
],
"permissions":[
"tabs","<all_urls>"
],
"version":"1"
}
background.js
function storeData(){
localStorage["domain"] = "google"; //Save the site to local storage for retrieval later when requested by the content script
localStorage["page"] = "Google"; //Save the page title to local storage for retrieval later
localStorage["url"] = "https://www.google.co.in/"; //Save the URL of the current page to local storage for retrieval
}
chrome.extension.onMessage.addListener(
function(request, sender, sendResponse)
{
console.log("request recieved is "+request);
if (request.name == "url")
{
console.log("sending response for request URL"+ localStorage["url"]);
sendResponse({url: localStorage["url"]});
}
else
{
console.log("sending response for request URL"+ localStorage["page"]);
sendResponse({domain: localStorage["domain"] + ": " + localStorage["page"]});
}
}
);
window.onload = function(){
storeData();
}
scripts.js
function requestBackground(){
chrome.extension.sendMessage({name:"domain"},
function(response)
{
console.log("response recived for response.domain "+response.domain);
});
chrome.extension.sendMessage({name:"url"},
function(response)
{
console.log("response recived for last page visited: " + response.url);
});
}
window.onload = function(){
requestBackground();
}
Let me know if it still fails.

Resources