Skip to content

Commit

Permalink
feat: fast provider injection 🔥 (#2691)
Browse files Browse the repository at this point in the history
* feat: add webln

* fix: all_frames property name

* feat: use service worker instead of manifest

* feat: inject content scripts on start

* feat: inline and inject provider in mv2 via custom webpack plugin
intercept build files add provider during build time and inject
signed-off-by: pavan joshi <[email protected]>

* chore: comment/refactor

* fix: use helper function

* fix: move injection from onend to onstart

* fix: remove injectScripts, separate WebLN from index

* fix: add inpage script registration for other providers

* fix: added todo

* fix: remove web accessible resources

* fix: separate index from webln

* fix: rename scripts, merge manifest content script definition

* feat: rewrite webpack plugin
inject all the provider via single variable
solve plugin error, while inlinining all the scripts in single variable
signed-off-by: pavan joshi <[email protected]>

* feat: add webln

* fix: all_frames property name

* feat: use service worker instead of manifest

* feat: inject content scripts on start

* feat: inline and inject provider in mv2 via custom webpack plugin
intercept build files add provider during build time and inject
signed-off-by: pavan joshi <[email protected]>

* chore: comment/refactor

* fix: use helper function

* fix: move injection from onend to onstart

* fix: remove injectScripts, separate WebLN from index

* fix: add inpage script registration for other providers

* fix: added todo

* fix: remove web accessible resources

* fix: separate index from webln

* fix: rename scripts, merge manifest content script definition

* feat: merge all the providers into single script

* chore: move function into separate file

* chore: place comment

* feat: do brower side checks in the inpage script itself
do browser side checks + extension side cheks such as blocklist in the provider's content script
signed-off-by: pavan joshi <[email protected]>

* feat: refactor clickhandlers in separate function
remove document check in inpage index
signed-off-by: pavan joshi <[email protected]>

* chore: change plugin variable names

* feat: be defensive about window and doctype are available or not

* fix: add fallback injection for blocked inline scripts

---------

Co-authored-by: pavanjoshi914 <[email protected]>
Co-authored-by: Pavan Joshi <[email protected]>
  • Loading branch information
3 people authored Aug 28, 2023
1 parent 50558e2 commit f98edca
Show file tree
Hide file tree
Showing 21 changed files with 183 additions and 161 deletions.
36 changes: 36 additions & 0 deletions build-utils/ProviderInjectionPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const { sources } = require("webpack");

const PLUGIN_NAME = "ProviderInjectionPlugin";
const PROVIDER_SCRIPT_FILENAME = "js/inpageScript.bundle.js";
const CONTENT_SCRIPT_FILENAME = "js/contentScriptOnStart.bundle.js";

// plugin to intercept webln bundle during built times and inline/attach provider to make it available immediately
// works only on MV2 and designed specifically for firefox as main world execution is not supported for firefox
class ProviderInjectionPlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.processAssets.tap(
{
name: PLUGIN_NAME,
stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE,
},
(assets) => {
let providerScriptSource =
assets[PROVIDER_SCRIPT_FILENAME].source().toString();
providerScriptSource = JSON.stringify(providerScriptSource);
let contentScriptSource =
assets[CONTENT_SCRIPT_FILENAME].source().toString();

assets[CONTENT_SCRIPT_FILENAME] = new sources.RawSource(
contentScriptSource.replace(
'"@@@WINDOW_PROVIDER@@@"',
providerScriptSource
)
);
}
);
});
}
}

module.exports = ProviderInjectionPlugin;
4 changes: 4 additions & 0 deletions src/extension/background-script/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import browser, { Runtime, Tabs } from "webextension-polyfill";
import utils from "~/common/lib/utils";

import { isManifestV3 } from "~/common/utils/mv3";
import { registerInPageContentScript } from "~/extension/background-script/registerContentScript";
import { ExtensionIcon, setIcon } from "./actions/setup/setIcon";
import { db, isIndexedDbAvailable } from "./db";
import * as events from "./events";
Expand Down Expand Up @@ -141,6 +143,8 @@ browser.runtime.onInstalled.addListener(handleInstalled);
async function init() {
console.info("Loading background script");

if (isManifestV3) registerInPageContentScript();

await state.getState().init();
console.info("State loaded");

Expand Down
15 changes: 15 additions & 0 deletions src/extension/background-script/registerContentScript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const registerInPageContentScript = async () => {
try {
await chrome.scripting.registerContentScripts([
{
id: "inpageScript",
matches: ["file://*/*", "http://*/*", "https://*/*"],
js: ["js/inpageScript.bundle.js"],
runAt: "document_start",
world: "MAIN",
},
]);
} catch (err) {
console.warn(`Dropped attempt to register inpage content script. ${err}`);
}
};
File renamed without changes.
29 changes: 23 additions & 6 deletions src/extension/content-script/injectScript.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
// load the inpage scripts
// only an inpage script gets access to the document

// and the document can interact with the extension through the inpage script
export default function injectScript(url: string) {
export function injectScript(script: string) {
try {
if (!document) throw new Error("No document");
const container = document.head || document.documentElement;
if (!container) throw new Error("No container element");
const scriptEl = document.createElement("script");
scriptEl.setAttribute("async", "false");
scriptEl.setAttribute("type", "text/javascript");
scriptEl.setAttribute("src", url);
scriptEl.textContent = script;
container.insertBefore(scriptEl, container.children[0]);
scriptEl.onload = () => {
container.removeChild(scriptEl);
};
container.removeChild(scriptEl);
} catch (err) {
console.error("injection failed", err);
console.error("Alby: provider injection failed", err);
}
}

export function injectScriptByUrl(url: string) {
try {
if (!document) throw new Error("No document");
const container = document.head || document.documentElement;
if (!container) throw new Error("No container element");
const scriptEl = document.createElement("script");
scriptEl.setAttribute("async", "false");
scriptEl.setAttribute("type", "text/javascript");
scriptEl.src = url;
container.insertBefore(scriptEl, container.children[0]);
container.removeChild(scriptEl);
} catch (err) {
console.error("Alby: provider injection failed", err);
}
}

export default { injectScript, injectScriptByUrl };
File renamed without changes.
File renamed without changes.
40 changes: 11 additions & 29 deletions src/extension/content-script/onstart.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,18 @@
import browser from "webextension-polyfill";
import api from "~/common/lib/api";

import injectScript from "./injectScript";
import shouldInject from "./shouldInject";
import { isManifestV3 } from "~/common/utils/mv3";
import {
injectScript,
injectScriptByUrl,
} from "~/extension/content-script/injectScript";

async function onstart() {
// eslint-disable-next-line no-console
const inject = await shouldInject();
if (!inject) {
return;
}

const account = await api.getAccount();
// window.webln
injectScript(browser.runtime.getURL("js/inpageScriptWebLN.bundle.js"));

// window.alby
injectScript(browser.runtime.getURL("js/inpageScriptAlby.bundle.js"));

// window.webbtc
if (account.hasMnemonic) {
injectScript(browser.runtime.getURL("js/inpageScriptWebBTC.bundle.js"));
}

// window.nostr
if (account.nostrEnabled) {
injectScript(browser.runtime.getURL("js/inpageScriptNostr.bundle.js"));
}

// window.liquid
if (account.liquidEnabled) {
injectScript(browser.runtime.getURL("js/inpageScriptLiquid.bundle.js"));
// Inject in-page scripts for MV2
if (!isManifestV3) {
// Try to inject inline
injectScript("@@@WINDOW_PROVIDER@@@");
// Fallback if inline script is blocked via CSP
injectScriptByUrl(browser.runtime.getURL("js/inpageScript.bundle.js"));
}
}

Expand Down
40 changes: 5 additions & 35 deletions src/extension/content-script/shouldInject.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import msg from "../../common/lib/msg";
import {
doctypeCheck,
documentElementCheck,
suffixCheck,
} from "../shouldInjectBrowserChecks";

// https://github.com/joule-labs/joule-extension/blob/develop/src/content_script/shouldInject.ts
// Whether or not to inject the WebLN listeners
Expand All @@ -11,41 +16,6 @@ export default async function shouldInject() {
return notBlocked && isHTML && noProhibitedType && hasDocumentElement;
}

// Checks the doctype of the current document if it exists
function doctypeCheck() {
const doctype = window.document.doctype;
if (doctype) {
return doctype.name === "html";
} else {
return true;
}
}

// Returns whether or not the extension (suffix) of the current document is prohibited
function suffixCheck() {
const prohibitedTypes = [/\.xml$/, /\.pdf$/];
const currentUrl = window.location.pathname;
for (const type of prohibitedTypes) {
if (type.test(currentUrl)) {
return false;
}
}
return true;
}

// Checks the documentElement of the current document
function documentElementCheck() {
// todo: correct?
if (!document || !document.documentElement) {
return false;
}
const docNode = document.documentElement.nodeName;
if (docNode) {
return docNode.toLowerCase() === "html";
}
return true;
}

async function blocklistCheck() {
try {
const currentHost = window.location.host;
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import browser from "webextension-polyfill";

import extractLightningData from "./batteries";
import injectScript from "./injectScript";
import getOriginData from "./originData";
import shouldInject from "./shouldInject";

Expand Down Expand Up @@ -33,8 +32,6 @@ async function init() {
return;
}

injectScript(browser.runtime.getURL("js/inpageScript.bundle.js")); // registers the DOM event listeners and checks webln again (which is also loaded onstart

browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
// extract LN data from websites
if (request.action === "extractLightningData") {
Expand Down
5 changes: 0 additions & 5 deletions src/extension/inpage-script/alby.js

This file was deleted.

45 changes: 26 additions & 19 deletions src/extension/inpage-script/index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
import { ABORT_PROMPT_ERROR, USER_REJECTED_ERROR } from "~/common/constants";
import AlbyProvider from "~/extension/providers/alby";
import LiquidProvider from "~/extension/providers/liquid";
import NostrProvider from "~/extension/providers/nostr";
import WebBTCProvider from "~/extension/providers/webbtc";
import WebLNProvider from "~/extension/providers/webln";
import shouldInjectInpage from "./shouldInject";

import WebLNProvider from "../providers/webln";

if (document) {
// window.webln is normally loaded onstart (see onstart.js)
// this is just to make double sure we load it
if (!window.webln) {
window.webln = new WebLNProvider();
}

function init() {
const inject = shouldInjectInpage();
if (!inject) return;
window.liquid = new LiquidProvider();
window.alby = new AlbyProvider();
window.nostr = new NostrProvider();
window.webbtc = new WebBTCProvider();
window.webln = new WebLNProvider();
const readyEvent = new Event("webln:ready");
window.dispatchEvent(readyEvent);

registerLightningLinkClickHandler();

// Listen for webln events from the extension
// emit events to the websites
window.addEventListener("message", (event) => {
if (event.source === window && event.data.action === "accountChanged") {
eventEmitter(event.data.action, event.data.scope);
}
});
}
function registerLightningLinkClickHandler() {
// Intercept any `lightning:` requests
window.addEventListener(
"click",
Expand Down Expand Up @@ -112,19 +128,10 @@ if (document) {
},
{ capture: true }
);
// Listen for webln events from the extension
// emit events to the websites
window.addEventListener("message", (event) => {
if (event.source === window && event.data.action === "accountChanged") {
eventEmitter(event.data.action, event.data.scope);
}
});
} else {
console.warn("Failed to inject WebLN provider");
}

function eventEmitter(action, scope) {
if (window[scope] && window[scope].emit) {
window[scope].emit(action);
}
}
init();
5 changes: 0 additions & 5 deletions src/extension/inpage-script/liquid.js

This file was deleted.

5 changes: 0 additions & 5 deletions src/extension/inpage-script/nostr.js

This file was deleted.

13 changes: 13 additions & 0 deletions src/extension/inpage-script/shouldInject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {
doctypeCheck,
documentElementCheck,
suffixCheck,
} from "../shouldInjectBrowserChecks";
export default function shouldInjectInpage() {
const isHTML = doctypeCheck();
const noProhibitedType = suffixCheck();
const hasDocumentElement = documentElementCheck();
const injectedBefore = window.webln !== undefined;

return isHTML && noProhibitedType && hasDocumentElement && !injectedBefore;
}
5 changes: 0 additions & 5 deletions src/extension/inpage-script/webbtc.js

This file was deleted.

5 changes: 0 additions & 5 deletions src/extension/inpage-script/webln.js

This file was deleted.

33 changes: 33 additions & 0 deletions src/extension/shouldInjectBrowserChecks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Checks the doctype of the current document if it exists
export function doctypeCheck() {
if (window && window.document && window.document.doctype) {
return window.document.doctype.name === "html";
} else {
return true;
}
}

// Returns whether or not the extension (suffix) of the current document is prohibited
export function suffixCheck() {
const prohibitedTypes = [/\.xml$/, /\.pdf$/];
const currentUrl = window.location.pathname;
for (const type of prohibitedTypes) {
if (type.test(currentUrl)) {
return false;
}
}
return true;
}

// Checks the documentElement of the current document
export function documentElementCheck() {
// todo: correct?
if (!document || !document.documentElement) {
return false;
}
const docNode = document.documentElement.nodeName;
if (docNode) {
return docNode.toLowerCase() === "html";
}
return true;
}
Loading

0 comments on commit f98edca

Please sign in to comment.