Safari Reference Library Apple Developer
Search

Messages and Proxies

Code in your global HTML page and extension bars interacts with the Safari application; it can’t directly access the contents of a webpage loaded in a browser tab. Similarly, an injected script interacts with web content; it can’t access the same Safari extensions API as extension bars or a global page.

But it’s sometimes desirable to cross this boundary. You may have controls in an extension bar or the main Safari toolbar that you want to affect web content, for example, or you may have a large block of code or data in an extension bar or your global HTML page that you want to use from an injected script.

The solution is to pass messages between parts of your extension. Because your global HTML page and extension bars can’t address webpages directly, they send messages to the SafariWebPageProxy. Similarly, injected scripts can’t address the global HTML page or an extension bar directly, so they send messages to the SafariContentBrowserTabProxy.

Message Structure

A message is an event whose type is “message”. You send a message by calling dispatchMessage(name, data) and receive messages by registering a listener function for “message” events.

A message event has a name property and a message property, which are the name and data you pass in dispatchMessage.

This can be a little confusing, so it bears repeating: event.name is the message name, and event.message is the message data.

Message data is not limited to a single data type; it can be boolean, numeric, a string, an array, a RegExp object, or anything that conforms to the W3C standard for safe passing of structured cloned data. It can also be null, undefined, or left blank, in cases where the command needs no data.

For example, the following snippet sends an array in a message:

var myArray = ["a", "b", "c"];

safari.self.tab.dispatchMessage("passArray", myArray);

Sending Messages to an Injected Script

To send messages to an injected script, you call theSafariWebPageProxy object’s dispatchMessage() method. The proxy stands in for the web content, which can be accessed as the page property of a SafariBrowserTab object, which is in the tabs array or activeTab property of a SafariBrowserWindow object, so sending a message to a script takes the general form:

safari.application.activeBrowserWindow.activeTab.page.dispatchMessage("name", "data");

Another example would be:

safari.self.tabs[0].page.dispatchMessage(myMessageName,myData);

The second example sends a message to the page in the leftmost tab of the window containing the extension bar.

In order to receive the message, your injected script must have a listener function defined and registered for “message” events. The listener function is called for all messages, so it needs to check the message name to be sure it’s responding to the desired message. For example, the following function looks for an “activateMyScript” message, then parses the message content:

function handleMessage(msgEvent) {
   var messageName = msgEvent.name;
   var messageData = msgEvent.message;
   if (messageName === "activateMyScript")
       { if (messageData === "stop")
          stopIt();
         if (messageData === "start")
          startIt();
       }
    }

The listener function in an injected script is added as a listener for “message” events in the SafariContentWebPage object (safari.self):

safari.self.addEventListener("message", handleMessage, false);

Note: The addEventListener() method takes a string for the event type, the name of the listener function (not in quotes, and without the usual trailing ‚Äú()‚Äù), and a boolean that tells the system whether the listener function should be given the event in the capture phase or the bubble phase. This is usually set false.

Receiving Messages from an Injected Script

A receiver function in an extension bar or global HTML page behaves identically to a receiver function in a script (it checks the message name before evaluating the message). But instead of registering with the SafariContentWebPage, a receiver function in an extension bar or global HTML page can register for the event at the tab, window, or application level:

safari.application.activeBrowserWindow.activeTab.addEventListener("message", waitForMessage, false);

safari.application.activeBrowserWindow.addEventListener("message", waitForMessage, false);

safari.application.addEventListener("message", waitForMessage, false);

The message is sent first to the application, then filters down to the window and tab. At each level, the event is sent to listener functions registered with boolean true. If no one has claimed it, the message then bubbles up from the tab through the window and back to the application, this time for listeners registered with boolean false. In most cases, it makes no practical difference at which level you intercept the message.

To send a message to an extension bar or global HTML page from an injected script, the script dispatches the message to the tab proxy:

safari.self.tab.dispatchMessage("heyExtensionBar","Klaatu barata nikto");

Example: Calling a Function from an Injected Script

If an injected script makes use of a large block of code or an extensive table of data, it is more efficient to put the bulky code or data in an extension bar or a global HTML page than in the injected script.

The following example is an injected script, Injected.js, that makes a function call to a global HTML page, Global.html, using messages. To see the example in action, follow these steps:

  1. Create an extension folder using Extension Builder.

  2. Copy the listings into a text editor and save as Injected.js and Global.html.

  3. Drag Injected.js and Global.html into your extension folder.

  4. Click Extension Global Page in Extension builder and choose Global.html.

  5. Click New Script in End Scripts and choose Injected.js.

  6. Set the Extension Website Access level to All.

  7. Click Install.

Listing 11-1  Injected.js

var initialVal=1;
var calculatedVal=0 ;
 
function doBigCalc(theData) {
    safari.self.tab.dispatchMessage("calcThis",theData);
}
 
function getAnswer(theMessageEvent) {
   if (theMessageEvent.name === "theAnswer") {
      calculatedVal=theMessageEvent.message;
      console.log(calculatedVal);
   }
}
safari.self.addEventListener("message", getAnswer, false);
 
doBigCalc(initialVal);

Listing 11-2  Global.html

<!DOCTYPE HTML>
<html>
<head>
<title>global HTML page</title>
<script type="text/javascript">
 
function bigCalc(startVal, event) {
   //imagine hundreds of lines of code here...
   endVal = startVal + 2;
   //return to sender
   event.target.page.dispatchMessage("theAnswer", endVal);
}
 
function respondToMessage(theMessageEvent) {
    if(theMessageEvent.name === "calcThis")
    {
       var startVal=theMessageEvent.message;
       bigCalc(startVal, theMessageEvent);
    }
}
 
    safari.application.addEventListener("message",respondToMessage,false);
</script>
</head>
<body>
</body>
</html>

In the example just given, the final value of the calculation is logged to the webpage console. To see the log entry, choose Show Web Inspector in the Develop menu.

For an example that shows how to pass messages to a script from an extension bar, see “Message-Passing Example.”

Blocking Unwanted Content

Safari 5.0 and later (and other Webkit-based browsers) generates a “beforeload” event before loading each sub-resource belonging to a webpage. The “beforeload” event is generated before loading every script, iframe, image, or stylesheet specified in the webpage, for example.

To block content, your script must be run as a Start Script, so that it executes before the content is displayed.

If your script responds to a “beforeload” event by calling event.preventDefault(), the pending sub-resource is not loaded. This is a useful technique for blocking ads, as shown in Listing 11-3.

Listing 11-3  Example: blocking content

function blockAds() {
   var itsAnAd = event.url.match(/ads.example.com/i);
   if (itsAnAd) {
      event.preventDefault();
      }
}
 
document.addEventListener("beforeload", blockAds, true);

Note: The url property of the event is the URL of the pending resource.

Blocking unwanted content can require a fair amount of code or a large table of data (or both) to filter all unwanted content accurately. You should put large blocks of code or data in your global page, so they are loaded only once, not in a script that loads before every webpage.

This creates a problem. You can put the code in your global page and call it by passing a message, but dispatchMessage() is an asynchronous function call. You need to delay the resource load until you know whether to allow it.

To solve this problem, use the canLoad() function, which returns data and operates synchronously:

var myReply = safari.self.tab.canLoad(event, myMessageData);

This dispatches a message event synchronously. You pass the “beforeload” event and any message data. The name of the message is always “canLoad”. The message data can be anything you like, but probably includes the URL of the resource in question.

A listener function in your global page (or an extension bar) sees the “canLoad” message, determines whether the resource should be blocked, and sets event.message to a reply that tells your injected script what to do. The reply can be a string, an object, or anything that can be passed as message data.

You need to register a listener function for the “beforeload” event in your injected script and call canLoad() from the listener function, so it all takes place before the resource loads. To block a resource from loading, call event.preventDefault() from the listener function as well.

The following example listens for the “beforeload” event in an injected script and passes the resource URL to the global page in canLoad(). The listener function in the global page compares the URL to a list domains to exclude, and sets the message returned to “allow” or “block”. The listener function in the injected script waits for the returned value, then conditionally prevents the load.

This part goes in your injected Start Script:

function isItOkay() {
   var myMessageData = event.url;
   var theAnswer = safari.self.tab.canLoad(event, myMessageData);
   if (theAnswer == "block") {
      event.preventDefault();
      }
}
 
document.addEventListener("beforeload", isItOkay, true);

This part goes in your global HTML page:

function blockOrAllow(event) {
    if (event.name === "canLoad") {
       var itsAnAd = event.message.match(/ads.example.com/i);
       if (itsAnAd)
          {
          event.message = "block";
          } else {
          event.message = "allow";
          }
    }
}
 
safari.self.addEventListener("message", blockOrAllow, true);



Last updated: 2010-08-03

Did this document help you? Yes It's good, but... Not helpful...