-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathbackground.js
More file actions
353 lines (321 loc) · 14 KB
/
Copy pathbackground.js
File metadata and controls
353 lines (321 loc) · 14 KB
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
// Service worker initialization
let debounceTimeouts = {}; // Store timeouts for each tabId
let popupTabID = null;
let featuresToInjectInPopup = [];
// Load features dynamically from a JSON file
async function loadFeatures() {
try {
const response = await fetch(chrome.runtime.getURL("features.json")); // Fetch the features configuration file
return response.json(); // Return the parsed JSON
} catch (error) {
console.error("❤️ Error loading features:", error);
return []; // Return empty array to prevent further errors
}
}
// Store injected features by tabId using a Map
// Map key: tabId, value: Set of feature keys already injected
const bubbleTabs = new Map();
// check if a feature script has already been injected
async function isFeatureInjected(tabId, featureKey) {
try {
let response = await chrome.scripting.executeScript({
target: { tabId },
func: (key) => {
// Check if the global object and the specific feature key exist
return (
typeof window.loadedCodelessLoveScripts !== "undefined" &&
window.loadedCodelessLoveScripts[key] === "loaded"
);
},
args: [featureKey], // Pass the feature key to the tab context
});
// Return the result from the tab's execution context
return response[0].result;
} catch (error) {
console.error("❤️ Error checking if feature is injected:", error);
return false; // Default to false in case of an error
}
}
// Injects enabled features (CSS/JS) into the specified tab
async function injectFeatures(tabId, context = {}) {
try {
const { isBubbleEditor = false, isApprovedDomain = false } = context;
const featuresConfig = await loadFeatures();
if (!featuresConfig || featuresConfig.length === 0) {
console.warn("❤️ No features found to inject");
return;
}
const defaults = Object.fromEntries(featuresConfig.map(f => [f.key, f.default]));
const prefs = await chrome.storage.sync.get(defaults);
const missingDefaults = {};
for (const [key, defaultValue] of Object.entries(defaults)) {
if (!(key in prefs)) {
missingDefaults[key] = defaultValue;
}
}
if (Object.keys(missingDefaults).length > 0) {
await chrome.storage.sync.set(missingDefaults);
Object.assign(prefs, missingDefaults);
}
bubbleTabs.set(tabId, new Set());
const injectedFeatures = bubbleTabs.get(tabId);
console.log("❤️💉 Injecting following features: ", featuresConfig);
console.group();
for (const feature of featuresConfig) {
const isEnabled = prefs[feature.key] == true;
// --- RUNTIME/EDITOR INJECTION LOGIC ---
const isRuntimeOnlyFeature = feature.requires === "enable_runtime_features";//enable_runtime_features itself is needed in the editor to load domain candidates to be granted permission. All other features which require this feature are themselves only to run in runtime, though.
// If this is a runtime feature (`key` or `requires` is `enable_runtime_features`), never inject in editor
if (isBubbleEditor && isRuntimeOnlyFeature) {
console.log(`❤️💉 Skipping injection of runtime feature ${feature.key} because this is the editor.`);
continue;
}
// In runtime, (on approved domains) only inject runtime features
if (!isBubbleEditor && isApprovedDomain && !isRuntimeOnlyFeature && feature.key !== "enable_runtime_features") {
console.log(`❤️💉 Skipping injection of non-runtime feature ${feature.key} because this is runtime.`);
continue;
}
// If not in editor or approved domain, skip all
if (!isBubbleEditor && !isApprovedDomain) {
console.log(`❤️💉 Skipping injection of feature ${feature.key} because this domain is not approved.`);
continue;
}
if (isEnabled && !injectedFeatures.has(feature.key)) {
// Check if the tab still exists before injecting
let tabExists = true;
try {
await chrome.tabs.get(tabId);
} catch (e) {
tabExists = false;
}
if (!tabExists) {
console.warn(`❤️ Skipping injection for ${feature.key}: tab ${tabId} no longer exists.`);
continue;
}
if (feature.cssFile) {
try {
await chrome.scripting.insertCSS({
target: { tabId }, // Specify the target tab
files: [feature.cssFile], // CSS file to inject
});
} catch (cssError) {
console.warn(`❤️ Error injecting CSS for ${feature.key}:`, cssError);
// Continue to try JS injection even if CSS fails
}
}
// Inject JS if available
if (feature.jsFile) {
const alreadyInjected = await isFeatureInjected(tabId, feature.key);
if (!alreadyInjected) {
try {
console.log(`❤️💉 Injecting into ISOLATED world: ${feature.key}`);
await chrome.scripting.executeScript({
target: { tabId },
files: [feature.jsFile],
});
// Verify injection was successful
const featureIsInjected = await isFeatureInjected(tabId, feature.key);
if (!featureIsInjected) {
console.warn(`❤️ JS injection for ${feature.key} may not have been successful`);
}
} catch (scriptError) {
console.error(`❤️ Error injecting JS for ${feature.key}:`, scriptError);
chrome.tabs.get(tabId).then(tab =>
console.error('❤️ Injection failed on URL:', tab.url)
);
// Add detailed error logging
console.error(`❤️ Error details:`, {
message: scriptError.message,
stack: scriptError.stack,
tabId: tabId,
featureFile: feature.jsFile,
// Get current tab URL
currentTab: await chrome.tabs.get(tabId).then(tab => ({
url: tab.url,
status: tab.status
})).catch(e => `Failed to get tab: ${e.message}`),
// Check if we have host permission
hasHostPermission: await chrome.permissions.contains({
origins: [`https://*.bubble.io/*`, `https://*.bubble.is/*`]
}).catch(e => `Failed to check permissions: ${e.message}`)
});
// Continue with other features even if this one fails
}
}
}
// Mark as injected regardless of success to avoid repeated injection attempts
injectedFeatures.add(feature.key);
}
}
console.groupEnd();
} catch (error) {
console.error("❤️ Error in injectFeatures:", error);
}
}
// Clean up injected features when a tab is closed
chrome.tabs.onRemoved.addListener((tabId) => {
bubbleTabs.delete(tabId); // Remove the tab's entry from the Map
});
// Listen for when a tab is updated (e.g., reloaded, navigated)
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// Check if the tab has finished loading and is a Bubble editor page
if (changeInfo.status === "complete" && tab.url &&
(tab.url.includes("bubble.io") || tab.url.includes("bubble.is") || tab.url.match(/^https?:\/\//))) {
let isBubbleEditor = false;
let isApprovedDomain = false;
let currentDomain = "";
try {
const urlObj = new URL(tab.url);
currentDomain = urlObj.hostname;
// Only permit exact /page paths for editor
isBubbleEditor = urlObj.pathname === "/page";
// Check if domain is in approvedDomains
chrome.permissions.getAll((perms) => {
const approvedDomains = (perms.origins || [])
.map(origin => {
try {
return origin.replace(/^https?:\/\//, '').replace(/\/$|\*$/, '').replace(/\/$/, '');
} catch (e) { return origin; }
})
.filter(Boolean);
isApprovedDomain = approvedDomains.includes(currentDomain);
// Only proceed if in editor or approved domain
if (isBubbleEditor || isApprovedDomain) {
// Clear any existing debounce timeout for the tabId
if (debounceTimeouts[tabId]) {
clearTimeout(debounceTimeouts[tabId]);
}
debounceTimeouts[tabId] = setTimeout(() => {
injectFeatures(tabId, { isBubbleEditor, isApprovedDomain });
}, 1000);
}
});
} catch (e) {
// fallback: do nothing
}
}
});
/* Facilitate feature's injecting their own scripts into the main world */
/* Listen to feature scripts for a command to inject code into the page */
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "injectScriptIntoMainWorld") {
// First check if the tab is ready
chrome.tabs.get(sender.tab.id, (tab) => {
if (chrome.runtime.lastError || !tab.status || tab.status !== "complete") {
console.log("❤️ Tab not ready for script injection, waiting for load");
// Wait for tab to be ready
chrome.tabs.onUpdated.addListener(function listener(updatedTabId, changeInfo) {
if (updatedTabId === sender.tab.id && changeInfo.status === "complete") {
chrome.tabs.onUpdated.removeListener(listener);
injectScriptIntoMainWorld(sender.tab.id, message.jsFile)
.then(result => sendResponse(result))
.catch(error => sendResponse({ error: error.message }));
}
});
} else {
// Tab is ready, inject immediately
injectScriptIntoMainWorld(sender.tab.id, message.jsFile)
.then(result => sendResponse(result))
.catch(error => sendResponse({ error: error.message }));
}
});
return true; // Keep message channel open for async response
}
});
// Injects a script directly into the tab's context (the "main world")
function injectScriptIntoMainWorld(tabId, url) {
console.log(`❤️💉 Injecting into MAIN world: ${url}`);
const fullScriptUrl = chrome.runtime.getURL(url);
// First fetch the script content
return fetch(fullScriptUrl)
.then(response => response.text())
.then(scriptContent => {
// Then execute it in the page context
return chrome.scripting.executeScript({
target: { tabId },
world: "MAIN", // Explicitly specify main world
func: (code) => {
// Create a blob URL from the code
const blob = new Blob([code], { type: 'text/javascript' });
const scriptUrl = URL.createObjectURL(blob);
const script = document.createElement('script');
script.src = scriptUrl; // Use blob URL instead of inline script
script.type = 'text/javascript';
script.className = '❤️injected-script';
// Clean up the blob URL after the script loads
script.onload = () => URL.revokeObjectURL(scriptUrl);
document.documentElement.appendChild(script);
return 'injected successfully';
},
args: [scriptContent] // Pass the actual script content
});
})
.then(([result]) => {
console.log(`❤️ Script ${url} injection result:`, result.result);
return result.result;
})
.catch((error) => {
console.error("❤️ Error injecting script:", error);
throw error;
});
}
/* Facilitate feature's injecting their own scripts into the Extension UI world */
/* Listen for popup to tell us it's done loading */
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "popupReady") {
console.log("❤️ Popup is ready to receive injected scripts.");
// For popups, we don't have a tabId - we communicate via messages
// Process any pending injections by sending them to the popup
featuresToInjectInPopup.forEach(({ sender: originalSender, message: originalMessage, sendResponse: originalSendResponse }) => {
// Send the script/CSS info to the popup for it to load itself
chrome.runtime.sendMessage({
action: "loadFeatureInPopup",
jsFile: originalMessage.jsFile,
cssFile: originalMessage.cssFile
}, (response) => {
originalSendResponse(response);
});
});
}
});
/* Listen to feature scripts for a command to inject code into the popup */
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "injectScriptIntoExtensionUIWorld") {
// Prevent duplicate injections into the popup
const alreadyExists = featuresToInjectInPopup.some(
(item) => item.message.jsFile === message.jsFile
);
if (alreadyExists) {
console.log(`❤️ ${message.jsFile} is already in the injection list for the popup, skipping.`);
return true; // Acknowledge message, but do nothing.
}
// Add the feature to the list so that every time the popup is loaded, we can re-inject everything.
console.log(`❤️ Added ${message.jsfile} to the list of features to inject in the popup.`);
featuresToInjectInPopup.push({ sender, message, sendResponse });
// If the popup is currently loaded, inject now (Note: this is an unlikely case. Usually the popup won't load until later and all scrips will be loaded at that point.)
chrome.runtime.sendMessage({
action: "loadFeatureInPopup",
jsFile: message.jsFile,
cssFile: message.cssFile
}, (response) => {
if (!chrome.runtime.lastError) {
// Popup is open and received the message
console.log(`❤️ Injected ${message.jsFile} into currently open popup`);
sendResponse(response);
}
});
return true; // Keep message channel open for async response
}
});
// Clean up when tab is closed
chrome.tabs.onRemoved.addListener((tabId) => {
// Clear any pending timeouts
if (debounceTimeouts[tabId]) {
clearTimeout(debounceTimeouts[tabId]);
delete debounceTimeouts[tabId];
}
// Notify content scripts to clean up
chrome.tabs.sendMessage(tabId, { action: "cleanup" }).catch(() => {
// Ignore errors - tab is likely already closed
});
});