Universal Safari extension

Like a universal Turing machine, a universal extension is an extension that can reproduce the behavior of an arbitrary extension. A universal extension does not actually exist because extension bars and toolbar items cannot be created dynamically. These are however the only restrictions, and this extension is a universal extension with one bar and one toolbar item.

In the settings you can specify scripts to be used in the global page and in the extension’s bar. In these scripts it is possible to create context menus, toolbar menus, and toolbar popovers. The extension contains a file popover.html which includes the popover script specified in the settings and can be used to create popover content (see the example below). The unique identifier for the bar, the unique identifier for the the toolbar item, and the command identifier for the toolbar item are all "universal".

Notes

Source

This is the extension’s global page:

<!DOCTYPE html>
<html>
   <head>
      <script>
         if(safari.extension.settings.global) {
            var scriptElement = document.createElement("script");
            scriptElement.src = safari.extension.settings.global + "?time=" + new Date().getTime();
            document.head.appendChild(scriptElement);
         }
      </script>
   </head>
   <body></body>
</html>

Examples

Loading multiple global scripts

The universal extension only loads one global script, so a first step to make it more useful is to have that script load any number of other scripts.

var base = "http://localhost/~joeshmoe/scripts/";

function loadScripts() {
   for(var i = 0; i < arguments.length; i++) {
      var scriptElement = document.createElement("script");
      scriptElement.src = base + arguments[i];
      document.head.appendChild(scriptElement);
   }
}

loadScripts("globalScript1.js", "globalScript2.js", "globalScript3.js");

Loading external scripts and style sheets

This global script can be used to load content scripts and style sheets from arbitrary URLs with a variety of parameters. Note that these scripts will not be sandboxed and the global variables they define will be visible in other scripts. The only way to add a sandboxed script is to make the whole script into a string and use the addContentScript method directly.

function addContentScripts(scripts, whitelist, blacklist, runAtEnd, defer, async) {
   if(scripts.length === 0) return;
   var script = "function loadScripts(){for(var i=0;i<arguments.length;i++){var s=document.createElement('script');" + (async ? "s.async=true;" : "") + (defer ? "s.defer=true;" : "") + "s.src=arguments[i];document.documentElement.appendChild(s);}}loadScripts('" + scripts.join("','") + "');";
   return safari.extension.addContentScript(script, whitelist, blacklist, runAtEnd);
}
function addContentStyleSheets(stylesheets, whitelist, blacklist) {
   if(stylesheets.length === 0) return;
   var script = "function loadStyleSheets(){for(var i=0;i<arguments.length;i++){var s=document.createElement('link');s.rel='stylesheet';s.href=arguments[i];document.documentElement.appendChild(s);}}loadStyleSheets('" + stylesheets.join("','") + "');";
   return safari.extension.addContentStyleSheet(script, whitelist, blacklist);
}

// The script runBeforeScript.js will run on every page before the page’s own scripts
addContentScripts(["http://localhost/~joeshmoe/runBeforeScript.js"], [], [], false, false, false);

// The scripts local.js and remote.js will run on all nonsecure pages without delaying page loading
addContentScripts(["http://localhost/~joeshmoe/local.js", "http://example.com/remote.js"], [], ["https://*/*"], false, true, false);

// The style sheet pretty.css will be applied to all pages on the domain someuglysite.com
addContentStyleSheets(["http://localhost/~joeshmoe/pretty.css"], ["http://someuglysite.com/*"], []);

Using the toolbar item and creating popovers

The following code in the global script assigns a click action and a popover to the toolbar item:

function setPopover(popover) {
   for(var i = 0; i < safari.extension.toolbarItems.length; i++) {
      safari.extension.toolbarItems[i].popover = popover;
   }
}

var universalPopover = safari.extension.createPopover("universal", safari.extension.baseURI + "popover.html");
setPopover(universalPopover);

function onOpen(event) {
   if(event.target instanceof SafariBrowserWindow) {
      setPopover(universalPopover);
   }
}

function onCommand(event) {
   if(event.command === "universal") {
      alert("The toolbar item was clicked!");
   }
}

safari.application.addEventListener("open", onOpen, true);
safari.application.addEventListener("command", onCommand, false);

Disabling WebM playback

This global script redefines the canPlayType method of media elements so as to always return "" on WebM MIME types. This is especially useful for Perian users.

var script = "\
   var s = document.createElement('script');\
   s.textContent = '\
   HTMLMediaElement.prototype.canPlayTypeCopy = HTMLMediaElement.prototype.canPlayType;\
   HTMLMediaElement.prototype.canPlayType = function(type) {\
      if(/webm/.test(type)) return \"\";\
      else return this.canPlayTypeCopy.apply(this, [type]);\
   };';\
   document.documentElement.appendChild(s);";
safari.extension.addContentScript(script, [], [], false);

Blocking HTML5 media

This global script prevents HTML5 media elements from preloading until the “Play” button is clicked.

var whitelist = ["trustedsource.com", "anothertrustedsource.com"];

function canLoad(url) {
   for(var i = 0; i < whitelist.length; i++) {
      if(url.indexOf(whitelist[i]) !== -1) return true;
   }
   return false;
}

function respondToMessage(event) {
   if(event.name === "canLoad") event.message = canLoad(event.message);
}

var script = "document.addEventListener('beforeload',handleBeforeLoadEvent,true);function handleBeforeLoadEvent(event){if(!(event.target instanceof HTMLMediaElement)||safari.self.tab.canLoad(event,event.url))return;event.target.autoplay=false;event.target.preload='none';}";

safari.extension.addContentScript(script, [], [], false);
safari.application.addEventListener("message", respondToMessage, false);