-
Notifications
You must be signed in to change notification settings - Fork 2
/
port.js
420 lines (379 loc) · 16.4 KB
/
port.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
// Chrome to Safari port
// Author: Michael Gundlach (gundlach@gmail.com)
// License: GPLv3 as part of code.getadblock.com
// or MIT if GPLv3 conflicts with your code's license.
//
// Porting library to make Chrome extensions work in Safari.
// To use: Add as the first script loaded in your Options page,
// your background page, your Chrome manifest.json, and your
// Safari Info.plist (created by the Extensions Builder).
//
// Then you can use chrome.* APIs as usual, and check the SAFARI
// global boolean variable to see if you're in Safari or Chrome
// for doing browser-specific stuff. The safari.* APIs will
// still be available in Safari, and the chrome.* APIs will be
// unchanged in Chrome.
if (typeof SAFARI == "undefined") {
(function() {
// True in Safari, false in Chrome.
SAFARI = (function() {
if (typeof safari === "undefined" && typeof chrome === "undefined") {
// Safari bug: window.safari undefined in iframes with JS src in them.
// Must get it from an ancestor.
var w = window;
while (w.safari === undefined && w !== window.top) {
w = w.parent;
}
window.safari = w.safari;
}
return (typeof safari !== "undefined");
})();
// Safari 5.0 (533.x.x) with no menu support
LEGACY_SAFARI = SAFARI && (navigator.appVersion.match(/\sSafari\/(\d+)\./) || [null,0])[1] < 534;
// Safari 5.1 (534.x.x) with no undo support
LEGACY_SAFARI_51 = SAFARI && (navigator.appVersion.match(/\sSafari\/(\d+)\./) || [null,0])[1] <= 534;
if (SAFARI) {
var isOnGlobalPage = !!safari.extension.bars;
// Return the object on which you can add/remove event listeners.
// If there isn't one, don't explode.
var listeningContext = function() {
if (safari.self && safari.self.addEventListener)
return safari.self;
if (safari.application && safari.application.addEventListener)
return safari.application;
console.log("No add/remove event listener possible at this location!");
console.trace();
return {
addEventListener: function() {},
removeEventListener: function() {}
};
};
var listenFor = function(messageName, handler) {
var listener = function(messageEvent) {
if (messageEvent.name == messageName)
handler(messageEvent);
};
listeningContext().addEventListener("message", listener, false);
return listener;
};
var removeListener = function(listener) {
listeningContext().removeEventListener("message", listener, false);
};
// Return the object on which you can dispatch messages -- globally, or on the
// messageEvent if specified. If there isn't one, don't explode.
// Make this globally available (don't use 'var') as it is used outside port.js
dispatchContext = function(messageEvent) {
// Can we dispatch on the messageEvent target?
var m = messageEvent;
if (m && m.target && m.target.page && m.target.page.dispatchMessage)
return m.target.page;
// Are we in some context where safari.self works, whatever that is?
var s = safari.self;
if (s && s.tab && s.tab.dispatchMessage)
return s.tab;
// Are we in the global page sending to the active tab?
var b = (safari.application && safari.application.activeBrowserWindow);
var p = (b && b.activeTab && b.activeTab.page);
if (p && p.dispatchMessage)
return p;
console.log("No dispatchMessage possible at this location!");
console.trace();
return {
dispatchMessage: function(msg, data) {
console.warn("Failed to call dispatchMessage(", msg, ",", data, ")");
console.trace();
}
};
};
// Replace the 'chrome' object with a Safari adapter.
chrome = {
extension: {
getBackgroundPage: function() {
return safari.extension.globalPage.contentWindow;
},
getURL: function(path) {
return safari.extension.baseURI + path;
},
sendRequest: (function() {
// Where to call .dispatchMessage() when sendRequest is called.
var dispatchTargets = [];
if (!isOnGlobalPage) {
// In a non-global context, the dispatch target is just the local
// object that lets you call .dispatchMessage().
dispatchTargets.push(dispatchContext());
}
else {
// In the global context, we must call .dispatchMessage() wherever
// someone has called .onRequest(). There's no good way to get at
// them directly, though, so .onRequest calls *us*, so we get access
// to a messageEvent object that points to their page that we can
// call .dispatchMessage() upon.
listenFor("onRequest registration", function(messageEvent) {
var context = dispatchContext(messageEvent);
if (dispatchTargets.indexOf(context) == -1)
dispatchTargets.push(context);
});
}
// Dispatches a request to a list of recipients. Calls the callback
// only once, using the first response received from any recipient.
function theFunction(data, callback) {
var callbackToken = "callback" + Math.random();
// Dispatch to each recipient.
dispatchTargets.forEach(function(target) {
var message = {
data: data,
frameInfo: chrome._tabInfo.gatherFrameInfo(),
callbackToken: callbackToken
};
target.dispatchMessage("request", message);
});
// Listen for a response. When we get it, call the callback and stop
// listening.
var listener = listenFor("response", function(messageEvent) {
if (messageEvent.message.callbackToken != callbackToken)
return;
// Must wrap this call in a timeout to avoid crash, per Safari team
window.setTimeout(function() {
removeListener(listener);
}, 0);
if (callback)
callback(messageEvent.message.data);
});
}
return theFunction;
})(),
onRequest: {
addListener: function(handler) {
// If listening for requests from the global page, we must call the
// global page so it can get a messageEvent through which to send
// requests to us.
if (!isOnGlobalPage)
dispatchContext().dispatchMessage("onRequest registration", {});
listenFor("request", function(messageEvent) {
var request = messageEvent.message.data;
var sender = {}; // Empty in onRequest in non-global contexts.
if (isOnGlobalPage) { // But filled with sender data otherwise.
var tab = messageEvent.target;
var frameInfo = messageEvent.message.frameInfo;
chrome._tabInfo.notice(tab, frameInfo);
sender.tab = chrome._tabInfo.info(tab, frameInfo.visible);
// Filled with URL of frame.
sender.url = frameInfo.url;
}
var sendResponse = function(dataToSend) {
var responseMessage = { callbackToken: messageEvent.message.callbackToken, data: dataToSend };
dispatchContext(messageEvent).dispatchMessage("response", responseMessage);
};
handler(request, sender, sendResponse);
});
}
},
onRequestExternal: {
addListener: function() {
// CHROME PORT LIBRARY: onRequestExternal not supported.
}
}
},
runtime: {
getManifest: function() {
var xhr = new XMLHttpRequest();
xhr.open("GET", chrome.extension.getURL("manifest.json"), false);
xhr.send();
var object = JSON.parse(xhr.responseText);
return object;
}
},
// Helper object to ensure that tabs sending requests to the global page
// get some extra attributes for the global page to use:
// id: an ID assigned by us so we can refer to the tab by ID elsewhere.
// visible_url: the URL of the top-level frame in the tab, if any.
// invisible_url: the URL of the top-level frame in the invisible page
// being preloaded by the tab, if any (new in Safari 6.1).
//
// We are forced to store the *_url properties because the Safari API
// doesn't currently give us a messageEvent.target.page.url property. See
// Issue #29.
_tabInfo: {
// Returns an object that can be passed to chrome._tabInfo.notice().
//
// Called by a frame sending a request to the global page.
gatherFrameInfo: function() {
return {
visible: !document.hidden,
top_level: (window === window.top),
url: document.location.href
};
},
// Tab objects are destroyed when no one has a reference to them, so we
// keep a list of them, lest our data get lost.
_tabs: [],
_lastAssignedTabId: 0,
// Ensure |tab| has a .id property assigned, and possibly update its
// |visible_url| or |invisible_url| property.
//
// info is an object passed from the requesting frame, containing
// visible: whether the frame is on a visible or preloading page
// url: url of the frame
// top_level: true if the frame is top level in its page
//
// Called by the global page.
notice: function(tab, info) {
// Clean up closed tabs, to avoid memory bloat.
this._tabs = this._tabs.filter(function(t) { return t.browserWindow != null; });
if (tab.id == undefined) {
// New tab
tab.id = this._lastAssignedTabId + 1;
this._lastAssignedTabId = tab.id;
this._tabs.push(tab); // save so it isn't garbage collected, losing our data.
}
if (info.top_level) {
tab[info.visible ? 'visible_url' : 'invisible_url'] = getUnicodeUrl(info.url);
}
},
// Return an {id, url} object for the given tab's top level frame if
// visible is true, or the given tab's preloaded page's top level frame,
// if visible is false. Assumes this.notice() has been called for every
// request from a frame to the global page.
//
// Called by the global page.
info: function(tab, visible) {
return {
id: tab.id,
url: (visible ? getUnicodeUrl(tab.visible_url) : getUnicodeUrl(tab.invisible_url))
};
}
},
i18n: (function() {
function syncFetch(file, fn) {
var xhr = new XMLHttpRequest();
xhr.open("GET", chrome.extension.getURL(file), false);
xhr.onreadystatechange = function() {
if(this.readyState == 4 && this.responseText != "") {
fn(this.responseText);
}
};
try {
xhr.send();
}
catch (e) {
// File not found, perhaps
}
}
// Insert substitution args into a localized string.
function parseString(msgData, args) {
// If no substitution, just turn $$ into $ and short-circuit.
if (msgData.placeholders == undefined && args == undefined)
return msgData.message.replace(/\$\$/g, '$');
// Substitute a regex while understanding that $$ should be untouched
function safesub(txt, re, replacement) {
var dollaRegex = /\$\$/g, dollaSub = "~~~I18N~~:";
txt = txt.replace(dollaRegex, dollaSub);
txt = txt.replace(re, replacement);
// Put back in "$$" ("$$$$" somehow escapes down to "$$")
var undollaRegex = /~~~I18N~~:/g, undollaSub = "$$$$";
txt = txt.replace(undollaRegex, undollaSub);
return txt;
}
var $n_re = /\$([1-9])/g;
var $n_subber = function(_, num) { return args[num - 1]; };
var placeholders = {};
// Fill in $N in placeholders
for (var name in msgData.placeholders) {
var content = msgData.placeholders[name].content;
placeholders[name.toLowerCase()] = safesub(content, $n_re, $n_subber);
}
// Fill in $N in message
var message = safesub(msgData.message, $n_re, $n_subber);
// Fill in $Place_Holder1$ in message
message = safesub(message, /\$(\w+?)\$/g, function(full, name) {
var lowered = name.toLowerCase();
if (lowered in placeholders)
return placeholders[lowered];
return full; // e.g. '$FoO$' instead of 'foo'
});
// Replace $$ with $
message = message.replace(/\$\$/g, '$');
return message;
}
var l10nData = undefined;
var theI18nObject = {
// chrome.i18n.getMessage() may be used in any extension resource page
// without any preparation. But if you want to use it from a content
// script in Safari, the content script must first run code like this:
//
// get_localization_data_from_global_page_async(function(data) {
// chrome.i18n._setL10nData(data);
// // now I can call chrome.i18n.getMessage()
// });
// // I cannot call getMessage() here because the above call
// // is asynchronous.
//
// The global page will need to receive your request message, call
// chrome.i18n._getL10nData(), and return its result.
//
// We can't avoid this, because the content script can't load
// l10n data for itself, because it's not allowed to make the xhr
// call to load the message files from disk. Sorry :(
_getL10nData: function() {
var result = { locales: [] };
// == Find all locales we might need to pull messages from, in order
// 1: The user's current locale, converted to match the format of
// the _locales directories (e.g. "en-US" becomes "en_US"
result.locales.push(navigator.language.replace('-', '_'));
// 2: Perhaps a region-agnostic version of the current locale
if (navigator.language.length > 2)
result.locales.push(navigator.language.substring(0, 2));
// 3: Set English 'en' as default locale
if (result.locales.indexOf("en") == -1)
result.locales.push("en");
// Load all locale files that exist in that list
result.messages = {};
for (var i = 0; i < result.locales.length; i++) {
var locale = result.locales[i];
var file = "_locales/" + locale + "/messages.json";
// Doesn't call the callback if file doesn't exist
syncFetch(file, function(text) {
result.messages[locale] = JSON.parse(text);
});
}
return result;
},
// Manually set the localization data. You only need to call this
// if using chrome.i18n.getMessage() from a content script, before
// the first call. You must pass the value of _getL10nData(),
// which can only be called by the global page.
_setL10nData: function(data) {
l10nData = data;
},
getMessage: function(messageID, args) {
if (l10nData == undefined) {
// Assume that we're not in a content script, because content
// scripts are supposed to have set l10nData already
chrome.i18n._setL10nData(chrome.i18n._getL10nData());
}
if (typeof args == "string")
args = [args];
for (var i = 0; i < l10nData.locales.length; i++) {
var map = l10nData.messages[l10nData.locales[i]];
// We must have the locale, and the locale must have the message
if (map && messageID in map)
return parseString(map[messageID], args);
}
return "";
}
};
return theI18nObject;
})()
};
//the property id is defined here so that we can invoke it without
//the need for parentheses
//for example `chrome.runtime.id`
//should return "com.betafish.adblockforsafari-UAMUU452D9"
Object.defineProperty(chrome.runtime, "id", {
get: function() {
return parseUri(safari.extension.baseURI).host;
},
set: undefined
});
}
})(); } // end if (typeof SAFARI == "undefined") { (function() {