-
Notifications
You must be signed in to change notification settings - Fork 214
/
Copy pathUserscriptsSafari.js
167 lines (159 loc) · 6.08 KB
/
UserscriptsSafari.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
// store code data received
var data;
// var that determines whether strict csp injection has already run (JS only)
var evalJS = 0;
// returns a sorted object
function sortByWeight(obj) {
var sorted = {};
Object.keys(obj).sort(function(a, b) {
return obj[b].weight - obj[a].weight;
}).forEach(function(key) {
sorted[key] = obj[key];
});
return sorted;
}
function injectCSS(filename, code) {
// there's no fallback if blocked by CSP
// future fix?: https://wicg.github.io/construct-stylesheets/
const tag = document.createElement("style");
tag.textContent = code;
document.head.appendChild(tag);
console.info(`Injecting ${filename}`);
}
function injectJS(filename, code, scope) {
code = code + "\n//# sourceURL=" + filename.replace(/\s/g, "-");
if (scope != "content") {
const tag = document.createElement("script");
tag.textContent = code;
document.body.appendChild(tag);
} else {
eval(code);
}
console.info(`Injecting ${filename}`);
}
function processJS(filename, code, scope, timing) {
// this is about to get ugly
if (timing === "document-start") {
if (document.readyState === "loading") {
document.addEventListener("readystatechange", function() {
if (document.readyState === "interactive") {
injectJS(filename, code, scope);
}
});
} else {
injectJS(filename, code, scope);
}
} else if (timing === "document-end") {
if (document.readyState !== "loading") {
injectJS(filename, code, scope);
} else {
document.addEventListener("DOMContentLoaded", function() {
injectJS(filename, code, scope);
});
}
} else if (timing === "document-idle") {
if (document.readyState === "complete") {
injectJS(filename, code, scope);
} else {
document.addEventListener("readystatechange", function(e) {
if (document.readyState === "complete") {
injectJS(filename, code, scope);
}
});
}
}
}
// parse data and run injection methods
function parseCode(data, fallback = false) {
// get css / js code separately
for (const type in data) {
// get the nested code object respectively
const codeTypeObject = data[type];
// will be used for ordered code injection
var sorted = {};
// css and js is injected differently
if (type === "css") {
sorted = sortByWeight(codeTypeObject);
for (const filename in sorted) {
const code = sorted[filename]["code"];
// css is only injected into the page scope after DOMContentLoaded event
if (document.readyState !== "loading") {
injectCSS(filename, code);
} else {
document.addEventListener("DOMContentLoaded", function() {
injectCSS(filename, code);
});
}
}
} else if (type === "js") {
// js code can be scoped to the content script, page or auto
// if auto is set, page scope is attempted, if fails content scope attempted
for (scope in codeTypeObject) {
// get the nested scoped objects, separated by timing
const scopedObject = codeTypeObject[scope];
// possible execution timings
const timings = ["document-start", "document-end", "document-idle"];
// check scopedObject for code by timing
timings.forEach(function(t) {
// get the nested timing objects, separated by filename
var timingObject = scopedObject[t];
// if empty, skip
if (Object.keys(timingObject).length != 0) {
sorted = sortByWeight(timingObject);
for (filename in sorted) {
const code = sorted[filename]["code"];
// scripts with auto scope will retry inject into content scope
// when blocked by strict CSP
if (fallback) {
console.warn(`Attempting fallback injection for ${filename}`);
scope = "content";
}
processJS(filename, code, scope, t);
}
}
});
}
}
}
}
// attempt to ensure script only runs on top-level pages
if (window.top === window) {
// request saved script code
safari.extension.dispatchMessage("REQ_USERSCRIPTS");
// attempt to detect strict CSPs
document.addEventListener("securitypolicyviolation", function(e) {
const src = e.sourceFile.toUpperCase();
const ext = safari.extension.baseURI.toUpperCase();
// eval fallback
// ensure that violation came from the extension
if ((ext.startsWith(src) || src.startsWith(ext))) {
// get all "auto" code
if (Object.keys(data["js"]["auto"]).length != 0 && evalJS < 1) {
var n = {"js":{"auto":{}}};
n["js"]["auto"] = data["js"]["auto"];
parseCode(n, true);
evalJS = 1;
}
}
});
}
// respond to messages
function handleMessage(event) {
// the only message currently sent to the content script
if (event.name === "RESP_USERSCRIPTS") {
// if error returned, log and stop execution
if (event.message.error) {
console.error(event.message.error);
return;
}
// save data sent with message to var
data = event.message.data;
// run injection sequence
// check if data is empty
if (Object.keys(data).length != 0) {
parseCode(data);
}
}
}
// event listener to handle messages
safari.self.addEventListener("message", handleMessage);