Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save cheadrian/8424217dda5a28724f290eef7fb4e059 to your computer and use it in GitHub Desktop.
Save cheadrian/8424217dda5a28724f290eef7fb4e059 to your computer and use it in GitHub Desktop.
Insert Firebase inside website using Chrome Extension with Manifest V3. Web Version 9 CDN modular Firebase Firestore local JS.

Chrome Extension Manifest V3 Firebase Javascript direct website inject

Updated: 12.Feb.2024

MV3 doesn't support remote hosted code anymore so everything should be bind inside extension package.

Save Firebase Javascript modules inside /js/firebase/ of extension root.

E.g. files: https://www.gstatic.com/firebasejs/10.8.0/firebase-app.js

https://www.gstatic.com/firebasejs/10.8.0/firebase-firestore.js

⚠️ Modify all firebase JS files to ensure all imports are from extension package, as remote script inserting is not allowed anymore in Manifest V3.

E.g. for this case firebase-firestore.js at end of line #1 :

from 'https://www.gstatic.com/firebasejs/10.8.0/firebase-app.js' 
to 
from './firebase-app.js';

Then add firebase_config.js from below to the same folder /js/firebase.

Add inject.js inside root of package.

//This file will be injected before body tag.
//Add file to src or modify path accordingly in manifest.json
import { initializeApp } from "./firebase-app.js";
import { getFirestore, collection, query, where, getDocs, addDoc } from "./firebase-firestore.js";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "XXXXXXXXXXXXXXX",
authDomain: "XXXXXX.firebaseapp.com",
projectId: "XXXXXX",
storageBucket: "XXXXXXX.appspot.com",
messagingSenderId: "XXXXXXXX",
appId: "1:XXXXXXXXXX:web:XXXXXXXXXXXXX"
};
const firebase_app = !getApps().length ? initializeApp(firebaseConfig) : getApp();
const db = getFirestore(firebase_app);
async function get_database_elements(db_name){
const q = query(collection(db, db_name));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
console.log(doc.id, " => ", doc.data());
});
}
//In order to use firebase_app and db inside the injected website
//pass to global scope is needed, because in module it has local scope
globalThis.firebase_app = firebase_app;
globalThis.db = db;
globalThis.get_database_elements = get_database_elements;
//This will inject the firebase module loader script.
function injectModule(file, node) {
var th = document.getElementsByTagName(node)[0];
var s = document.createElement('script');
s.setAttribute('type', 'module');
s.setAttribute('src', file);
th.appendChild(s);
}
injectModule(chrome.runtime.getURL('js/firebase/firebase_config.js'), 'body');
{
"name": "Example",
"version": "1.0.0",
"manifest_version": 3,
"description": "Example.",
"externally_connectable": {
"matches": [ "https://example.com/*" ]
},
"default_locale": "en",
"content_scripts": [
{
"matches": [
"https://example.com/*"
],
"js": [
"inject.js"
],
"run_at": "document_idle",
"all_frames": true
}
],
"web_accessible_resources": [
{
"resources":[
"js/firebase/firebase_config.js",
"js/firebase/firebase-app.js",
"js/firebase/firebase-firestore.js",
"js/firebase/firebase-database.js"
],
"matches": [ "https://example.com/*" ]
}
]
}
@noovil
Copy link

noovil commented Jan 7, 2022

Hey thanks for this, but I get an error "Uncaught Error: Service firestore is not available" at line 20 of firebase_config. Any idea why?

@cheadrian
Copy link
Author

@noovil did you modify the firebase-firestore.js line one according to .md?
You can't import anything remotely with manifest V3, except localhost.

@noovil
Copy link

noovil commented Jan 10, 2022

@cheadrian Hey, indeed I forgot to change the first line.

@bhdrozgn
Copy link

bhdrozgn commented Apr 7, 2022

I am injecting your firebase_config.js and my own script in a site, but I can't access the global variables in my own script, they are shown as undefined.

Edit: Instead using global variables, I did it by sending custom events from my injected script to my content script, and from the content script I transferred the event to background script. Then I sent the response from background script to my content script and from there to my injected script.

In short, I used the content script as an intermediate forwarder between background and injected script.

@amervelic
Copy link

amervelic commented Jun 5, 2022

"Modify all firebase JS files to ensure all imports are from extension package, as remote script inserting is not allowed anymore in Manifest V3." In your case, it is ALLOWED. It is NOT allowed in the service worker. Inject inserts a tag script and it works quite well with remote scripts and without local ones (firbase-app, etc).

@fitmintdotco
Copy link

thank you for this tip! i was able to integrate google sign-on with mv3 using this approach. nice!

@Adriman2
Copy link

@bhdrozgn could you provide an example of how you achieved this? Currently struggling with the same.

@bhdrozgn
Copy link

bhdrozgn commented Mar 30, 2023

@bhdrozgn could you provide an example of how you achieved this? Currently struggling with the same.

content-scripts.js

window.addEventListener("PassToBackground", (ev) => {
    if (ev.detail.command != "keep-awake") {
        console.log("sent", ev.detail);
    }
    chrome.runtime.sendMessage(ev.detail, (response) => {
        if (ev.detail.command != "keep-awake") {
            console.log("response", response);
        }
    });
}, false);

var script = document.createElement("script");
var url = window.location.href;
if (url.startsWith("https://www.randomwebsite.com/")) {
    script.src = chrome.runtime.getURL("js/randomwebsite.js");
}
script.onload = function() {
    this.remove();
};
(document.head || document.documentElement).appendChild(script);

I use an event listener in content-scripts.js to send structured events to background.js from randomwebsite.js which is the script injected to www.randomwebsite.com. You can ignore 'keep-awake' events. I just used them to keep background script awake by sending non-functional events every 5 seconds.

You can send an event in randomwebsite.js like:

const message = {
    command: "post",
    timestamp: Object.keys(data)[0],
    data: data
};
const event = new CustomEvent("PassToBackground", { detail: message });
window.dispatchEvent(event);

Finally, this is the background.js, where I handle every event sent here:

import {
    initializeApp
} from "../firebase/firebase-app.js";
import {
    getAuth,
    createUserWithEmailAndPassword,
    signInWithEmailAndPassword,
    signOut,
    updateProfile,
    onAuthStateChanged
} from "../firebase/firebase-auth.js";
import {
    getFirestore,
    collection,
    doc,
    setDoc,
    writeBatch,
} from "../firebase/firebase-firestore.js";

const firebaseConfig = {
    apiKey: "xxxxxxxx",
    authDomain: "xxxxxxxx",
    projectId: "xxxxxxxx",
    storageBucket: "xxxxxxxx",
    messagingSenderId: "xxxxxxxx",
    appId: "xxxxxxxx",
    measurementId: "xxxxxxxx"
};

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);

chrome.runtime.onMessage.addListener((msg, sender, resp) => {
    if (user || (msg.command != "post" && msg.command != "fetch" && msg.command != "fetch-all")) {
        access_db(msg, sender, resp, user?.uid);
    }
    return true;
});

async function access_db(msg, sender, resp) {
    if (msg?.command == 'post') {
        // firebase commands to store data to db
    }
}

Inversely, you can also get info from background.js and pass it to some script via content-scripts.js

@FarisHijazi
Copy link

FarisHijazi commented Apr 27, 2023

can't you just use self.importScripts in the service worker? instead of writing all that inject code?

  self.importScripts(
        "js/firebase/firebase_config.js",
        "js/firebase/firebase-app.js",
        "js/firebase/firebase-firestore.js",
        "js/firebase/firebase-database.js"
  );

@bhdrozgn
Copy link

bhdrozgn commented Apr 27, 2023

can't you just use self.importScripts in the service worker? instead of writing all that inject code?

  self.importScripts(
        "js/firebase/firebase_config.js",
        "js/firebase/firebase-app.js",
        "js/firebase/firebase-firestore.js",
        "js/firebase/firebase-database.js"
  );

I was developing an extension for manifest v3 for the first time and I just needed a few functions from firebase, this was what worked for me in the past. I don't know if yours will work or not, but worth trying.

@biplobsd
Copy link

@bhdrozgn How to configure in the Action script popup?

@sinanspd
Copy link

@bhdrozgn could you provide an example of how you achieved this? Currently struggling with the same.

content-scripts.js

window.addEventListener("PassToBackground", (ev) => {
    if (ev.detail.command != "keep-awake") {
        console.log("sent", ev.detail);
    }
    chrome.runtime.sendMessage(ev.detail, (response) => {
        if (ev.detail.command != "keep-awake") {
            console.log("response", response);
        }
    });
}, false);

var script = document.createElement("script");
var url = window.location.href;
if (url.startsWith("https://www.randomwebsite.com/")) {
    script.src = chrome.runtime.getURL("js/randomwebsite.js");
}
script.onload = function() {
    this.remove();
};
(document.head || document.documentElement).appendChild(script);

I use an event listener in content-scripts.js to send structured events to background.js from randomwebsite.js which is the script injected to www.randomwebsite.com. You can ignore 'keep-awake' events. I just used them to keep background script awake by sending non-functional events every 5 seconds.

You can send an event in randomwebsite.js like:

const message = {
    command: "post",
    timestamp: Object.keys(data)[0],
    data: data
};
const event = new CustomEvent("PassToBackground", { detail: message });
window.dispatchEvent(event);

Finally, this is the background.js, where I handle every event sent here:

import {
    initializeApp
} from "../firebase/firebase-app.js";
import {
    getAuth,
    createUserWithEmailAndPassword,
    signInWithEmailAndPassword,
    signOut,
    updateProfile,
    onAuthStateChanged
} from "../firebase/firebase-auth.js";
import {
    getFirestore,
    collection,
    doc,
    setDoc,
    writeBatch,
} from "../firebase/firebase-firestore.js";

const firebaseConfig = {
    apiKey: "xxxxxxxx",
    authDomain: "xxxxxxxx",
    projectId: "xxxxxxxx",
    storageBucket: "xxxxxxxx",
    messagingSenderId: "xxxxxxxx",
    appId: "xxxxxxxx",
    measurementId: "xxxxxxxx"
};

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);

chrome.runtime.onMessage.addListener((msg, sender, resp) => {
    if (user || (msg.command != "post" && msg.command != "fetch" && msg.command != "fetch-all")) {
        access_db(msg, sender, resp, user?.uid);
    }
    return true;
});

async function access_db(msg, sender, resp) {
    if (msg?.command == 'post') {
        // firebase commands to store data to db
    }
}

Inversely, you can also get info from background.js and pass it to some script via content-scripts.js

Would you mind sharing how you worked around imports not being allowed in anything other than modules?

@Christiannjhay
Copy link

can i have a look at your manifest

@cheadrian
Copy link
Author

cheadrian commented Feb 12, 2024

"Modify all firebase JS files to ensure all imports are from extension package, as remote script inserting is not allowed anymore in Manifest V3." In your case, it is ALLOWED. It is NOT allowed in the service worker. Inject inserts a tag script and it works quite well with remote scripts and without local ones (firbase-app, etc).

function injectScript(file, node) {
    var th = document.getElementsByTagName(node)[0];
    var s = document.createElement('script');
    s.setAttribute('type', 'text/javascript');
    s.setAttribute('src', file);
    th.appendChild(s);
}
injectScript('https://www.gstatic.com/firebasejs/10.8.0/firebase-app.js', 'head');
Refused to load the script 'https://www.gstatic.com/firebasejs/10.8.0/firebase-app.js' because it violates the following Content Security Policy directive: "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:*". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

Extension Pages Policy

The extension_pages policy cannot be relaxed beyond this minimum value.

You can relax these directives on the sandbox space, but this would require to implement the message API, like @bhdrozgn .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment