Safari extension send message to a specific tab - javascript

Is there a way to send a message from the global page to a specific tab?
What I'm currently doing is, on tab creation the injected script creates a unique id and sends a message with this number to the global page and the global page saves this number.
If the global page needs to send some data to a tab (i.e: tab #3) then the global page will "broadcast" a message to all tabs with the number #3 as part of the data passed to the tabs (iterate over all tabs and send a message to each tab).
Is there something like Chrome: (i.e: chrome.tabs.sendRequest(tabID, {action: 'respond', params:[channel,msg,async]});)?
Right now what I'm doing is that on the injected script side, each script has a listener that will catch this message. If a content script unique number is equal to the number sent by the global page then this message is for it, else doNothing.
Is there an easier more elegant way to do this in Safari?

Within the global page's message event handler, event.target refers to the tab from which a message was received. For example:
function handleMessage(e) {
if (e.name === 'whatIsMyUrl?') {
e.target.page.dispatchMessage('yourUrlIs', e.target.url);
}
}
safari.application.addEventListener("message", handleMessage, false);
The Safari extension API doesn't have tab IDs, but you could just keep each tab in an associative array and use its index to address it later. For example:
function handleMessage(e) {
if (e.name === 'hereIsMyId') {
myTabs[e.message] = e.target;
}
}
safari.application.addEventListener("message", handleMessage, false);
// later...
myTabs[someId].page.dispatchMessage('haveSomeCake');

Safari Answer
In your global page save directly to the tab.. so for instance on message from injected script
// global page
safari.application.addEventListener("message", function(event){
switch(event.name){
case "saveData":
event.target.page.tabData = { data: myData }
break;
case "getData":
event.target.page.dispatchMessage("tabData", myData);
break;
}
}, false);
-
// injected page
// first save data
safari.self.tab.dispatchMessage("saveData", {firstname:"mike", age: 25} );
// setup listner to recevie data
safari.self.addEventListener("message", function(event){
switch(event.name){
case "tabData":
// get data for page
console.debug(event.message);
// { firstname: "mike", age: 25 }
break;
}
}, false);
// send message to trigger response
safari.self.tab.dispatchMessage("getData", {} );

Related

Why does JavaScript "window.postMessage" create duplicate messages?

I'm trying to open a popup window, do some stuff, and send a message back to the opening window so I can do some more stuff with the data it sends.
Essentially, I'm adapting the process outlined here.
This is my code on the "opening window". It runs when a social connect button is clicked. The code opens a popup window and assigns a listener event on the opening window to receive a message from the popup:
//Do the operation
let windowObjectReference = null;
let previousUrl = null;
const openSignInWindow = (url, name) => {
// remove any existing event listeners
window.removeEventListener('message', receiveMessage);
// window features
const strWindowFeatures =
'toolbar=no, menubar=no, width=600, height=700, top=100, left=100';
if (windowObjectReference === null || windowObjectReference.closed) {
/* if the pointer to the window object in memory does not exist
or if such pointer exists but the window was closed */
windowObjectReference = window.open(url, name, strWindowFeatures);
} else if (previousUrl !== url) {
/* if the resource to load is different,
then we load it in the already opened secondary window and then
we bring such window back on top/in front of its parent window. */
windowObjectReference = window.open(url, name, strWindowFeatures);
windowObjectReference.focus();
} else {
/* else the window reference must exist and the window
is not closed; therefore, we can bring it back on top of any other
window with the focus() method. There would be no need to re-create
the window or to reload the referenced resource. */
windowObjectReference.focus();
}
// add the listener for receiving a message from the popup
window.addEventListener('message', event => receiveMessage(event), false);
// assign the previous URL
previousUrl = url;
};
const receiveMessage = event => {
// Do we trust the sender of this message? (might be different from what we originally opened, for example).
if (event.origin !== websiteHomeUrlNoSlash) {
return;
}
const { data } = event;
console.log(data); //<--- THIS WHERE I'm SEEING DUPLICATES
};
//Invoke the function
openSignInWindow(url, name);
In the popup users login to their social account and then get redirected to a page on my app where the below code is run. The code posts a message back to the opening window and then closes the popup:
// Get the message data
const messageObj = {
pluginReason: pluginReasonVar,
displayName: displayNameVar,
provider: providerVar,
};
if (window.opener) {
// send them to the opening window
window.opener.postMessage(messageObj, websiteHomeUrlNoSlash);
// close the popup
if (closePopup) {
window.close();
}
}
Everything almost works as expected. Users can login to their social accounts and all the redirections and opening and closing of the popup works fine.
The Problem:
If users go through the Social Connect process multiple times without refreshing the page, then the message data that is printed to the console is duplicated more and more each run.
For example:
On the 1st run console.log(data) is printed once. So far this works as expected.
On the 2nd run console.log(data) prints twice. It should only be printed once.
On the 3rd run console.log(data) prints three times. It should only be printed once.
Each time the Social Connect process is run it should only print once. But somehow it's adding a duplicate copy on each subsequent run.
This duplication keeps growing until the users refreshes the page, which starts the count back at one.
I want to do more data manipulation at the point of the console.log(data) but I can't do that while it's creating duplicates copies on each subsequent run.
How do I stop that from happening?
Maybe it's because the listener event is not detaching? If so, how do I fix that?
You have created an anonymous method (event) => { } as a wrapper and attached it to the addEventListener method.
window.addEventListener('message', event => receiveMessage(event), false);
It can't be removed by
window.removeEventListener('message', receiveMessage);
To fix it, make changes like this:
window.addEventListener('message', receiveMessage, false);
Meanwhile, if the method receiveMessage gets lost every time the window has been closed, it's better to move the removeEventListener part inside the receiveMessage.
const receiveMessage = (event)=> {
window.removeEventListener('message', receiveMessage);
// do something else
}

JavaScript: calling function of parent window from child window

I am opening a popup window from a page. The process goes like this:
var newwindow = window.open("mydomain.com/a.html", "Testing", 'height=600,width=800');
if (window.focus) { newwindow.focus() }
mydomain.com/a.html opens a popup mydomain.com/b.html
mydomain.com/b.html redirects the user to another site(say payment gateway)
paymentgateway.com/authenticate.html
paymentgateway.com/authenticate.html redirects the user to mydomain.com/success.html
From mydomain.com/success.html I want to execute a function written on mydomain.com/a.html
I have written
window.parent.LaunchFunction();
window.close();
but it's not working. What can be the issue? Is it possible to achieve?
I'm offering another way - use the local storage for communication between apps at the same origin (same protocol+domain+port).
You can add listener, that is fired when somebody change any property in localstorage.
First instance is listening
window.addEventListener('storage', onStorageChange, false);
function onStorageChange (e) {
if (e.key === 'messageText') {
var messageText = localStorage.getItem('messageText');
if (messageText) { // Prevent to call when fired by `removeItem()` below
console.log('New message from another instance has come:', messageText);
localStorage.removeItem('messageText'); // this will fires the `onStorageChange` again.
}
}
}
Second instance is sending the messages
localStorage.setItem('messageText', 'Hello from second app instance');

postMessage Function runs repeatedly without a message being sent

I have an iframe with a form in it, and in that iframe I have the following:
// Send a message to the parent window
window.parent.postMessage({
event: 'submit'
}, '*');
The above is supposed to send a message to the parent window when the form is submitted.
In the parent window I have the following:
function receiveMessage(event) {
var origin = event.origin;
if (origin !== 'https://iframe.domain') {
return;
} else {
console.log('Submitted!');
}
}
window.addEventListener('message', receiveMessage, false);
The problem I seem to be having is that the code on the parent window is executing immediately without a message being sent from the iframe form being submitted. It is also executing over and over again. It logs "Submitted!" in the console over and over again for as long as I let it run.
How can this function run without the form being submitted to send the function, and why is it running over and over again?
In my iframe I moved the postMessage() to the footer, and checked for a div that is only available after my form is submitted. If the div exists I send a message to the parent window. This is the exact code in my iframe now.
// if the form has submitted and the confirmation message
// div exists send a messge to the parent window
if (jQuery('#gform_confirmation_wrapper_1').length) {
// Send a message to the parent window
parent.postMessage({
event: 'formSubmit'
}, '*');
}
On my parent window I created the function, checked the domain the message was coming from, and checked for the exact message being sent with if (event.data.event === 'formSubmit'). If that message, which was only sent from my iframe if the form's confirmation div existed, matched exactly formSubmitted then I pushed the event to the datalayer of Google Tag Manager. This is the exact code that is working on my dev site now.
// create function to push event to GTM if the iframe form has been submitted
function awReceiveMessage(event) {
// set variable to url that the message is coming from
var origin = event.origin;
// check if the message is coming from Polk
if (origin !== 'https://iframe.domain') {
//stop function if it is not coming from Polk
return;
} else {
// instantiating the GTM datalayer variable
var dataLayer = window.dataLayer || (window.dataLayer = []);
// if the message is formSubmit push the formSubmit event to the GTM datalayer
if (event.data.event === 'formSubmit') {
dataLayer.push({
'event': 'formSubmit'
});
}
}
}
// run awReceiveMessage() if a message is sent from the iframe to the parent
window.addEventListener('message', awReceiveMessage, false);
The above code is working and firing a GTM tag correctly on the parent page when the form is submitted in the iframe.
You need to check you have received the correct message.
function receiveMessage(event) {
if (event.origin !== 'https://iframe.domain') {
return;
} else if (event.data.event && event.data.event === 'submit') {
console.log('Submitted!');
}
}
window.addEventListener('message', receiveMessage, false);
I find it odd you are getting so many messages and I would suggest adding some code to dump them to the console to see what they are.
window.addEventListener('message', (event) => console.log(event), false);

How can I prevent multiple injection/execution of content script(s) in response to a single event?

I am building a chrome extension that responds to click events on a context menu.
My background script creates the context menu using the chrome.contextMenus.create api method call and sets a click handler as shown in the code below:
//event.js
function onItemClick(info, tab){
// Inject the content script into the current page
chrome.tabs.executeScript(null, { file: 'content.js' });
// Perform the callback when a message is received from the content script
chrome.runtime.onMessage.addListener(function(message){
var url = "data:text/html;charset=utf8,";
function append(key, value){
var input = document.createElement('textarea');
input.setAttribute('name', key);
input.textContent = value;
form.appendChild(input);
}
var form = document.createElement('form');
form.method = 'POST';
form.action = 'http://localhost/myapp/myapp.php';
form.style.visibility = "hidden";
append('url', message.url);
append('text', message.selectedText);
url = url + encodeURIComponent(form.outerHTML);
url = url + encodeURIComponent('<script>document.forms[0].submit();</script>');
chrome.tabs.create({url: url, active: true});
});
}
var context = "selection";
var title = "Share in new tab";
var id = chrome.contextMenus.create({"title": title, "contexts": [context], "onclick": onItemClick});
The background script above programmatically creates a form that automatically gets submitted in a new tab. In doing so, it calls a "content script" below to get some information from the current page/tab.
//content.js
chrome.runtime.sendMessage({
'url': window.location.href,
'selectedText': window.getSelection().toString()
});
The problem is this. The click handler in the background script injects the "content script" into the current page multiple times (that is, every time the click handler is called). As a result of this multiple injection, each injected instance of the "content script" is executed resulting in multiple new tabs/pages being opened. The number of new tabs opened increases by one each time the context menu item is clicked suggesting the problem is indeed multiple injection and execution of the content script. How can I inject the content script only once, or at least ensure that only one "instance" of the injected scripts sends a message back to my background script?
I have tried to automatically inject the script in the manifest, but calling chrome.tabs.executeScript thereafter results in an endless creation of tabs. So, I really need to be able to inject the script on demand, but find a way to either prevent multiple injections or at least ensure only one "injection" sends a message back. Please help!
The solution can be achieved easily: you can create a global control variable in the content script. Check for it at the beginning, and if it is undefined then set it to true and proceed. The first content script that gets executed will set the variable and prevent others from doing anything.
By the way, I see you're adding a listener to chrome.runtime.onMessage inside another listener: that is not good practice, because it will add multiple listeners for the same event and result in executing them multiple times. You should instead declare the listener outside, sending a different message saying "do something" or "do something else".
In the content script:
if (window.messageSent === undefined) {
window.messageSent = true;
chrome.runtime.sendMessage({
action: "submit the form",
url: window.location.href,
selectedText: window.getSelection().toString()
});
}
In the background.js:
chrome.runtime.onMessage.addListener(function(message){
if (message.action == "submit the form") {
// do what you need to submit the form
var url = "data:text/html;charset=utf8,";
function append(key, value){
...
}
});

Chrome extension persistent popup best practices

I've understood from the docs that closing chrome extension popups when losing focus has been a design choice.
I'm working on an extension where the user chooses to save elements from a webpage. As he interacts with the main webpage I would like the popup to get updated but that's obviously not possible.
What's the proper way of handling this situation? (this is my first chrome extension)
You can have a content script detect the "save" action. Let's suppose it's a specific DOM element you know for sure it's going to be in the specific main, or that you create by yourself.
content.js
//content script
document.onreadystatechange = function () {
if (document.readyState == "complete") {
// Grab the UI frmo the mainpage you want to append the save functionality
var someElementsYouWantToAppendASaveButtonTo = document.getElementsByTagName("...");
var len = someElementsYouWantToAppendASaveButtonTo.length;
for (var i = 0; i < len; i++) {
// Create a UI save button to provide a functionality
var theSaveButton = document.createElement("button");
theSaveButton.value = "Save to Chrome Extension";
// Send data to extension when clicked
theSaveButton.addEventListener("click", function() {
var dataToSentToExtension = {...} // Retrieve from the clicked element, or whatever you want to save
chrome.extension.sendMessage(dataToSentToExtension, function(response) {
if(response.success) console.log("Saved successfully");
else console.log("There was an error while saving")
});
}, false);
someElementsYouWantToAppendASaveButtonTo[i].appendChild(theSaveButton)
}
}
}
Then, on the background, you detect the response and set up the popup as you wish.
background.js
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
if(request.dataToSave) {
chrome.storage.local.set(dataToSave, function() {...});
// You can then set upn the proper popup for the next click or even switch to it
switch(request.popupToDisplay) {
case "awesomeDisplay":
chrome.browserAction.setPopup({...})
break;
}
var responseFromExtension = {success: true}
} else {
var responseFromExtension = {error: true}
}
});
It seems you are looking to modify\update your popup.html page in accord to changes in a web page. If so, use content scripts and establish connection for single message communication with background page(Since focus is lost every time) and update popup.html indirectly.
References:
Content Scripts
Background Page
Message Passing
Reference for communication between popup and background page apart from these,
there are bunch of questions on these topics, they will get you started..

Categories

Resources