18

Sono un principiante nello sviluppo delle estensioni del browser e ho compreso il concetto di estensioni del browser che altera la pagina e inserisce codici in esso.Un sito può richiamare un'estensione del browser?

C'è un modo in cui questa direzione può essere invertita? Scrivo un'estensione che fornisce un insieme di API e i siti Web che desiderano utilizzare la mia estensione possono rilevare la sua presenza e, se è presente, il sito Web può chiamare i miei metodi API come var extension = Extenion(foo, bar). È possibile in Chrome, Firefox e Safari?

Esempio:

  1. Google ha creato una nuova estensione chiamata BeautifierExtension. Ha un set di API come oggetti JS.

  2. L'utente passa a reddit.com. Reddit.com rileva BeautifierExtension e invocare l'API chiamando beautifer = Beautifier();

vedi # 2 - normalmente è l'estensione che rileva i siti di corrispondenza e modificare le pagine. Quello che mi interessa sapere è se il n. 2 è possibile.

risposta

54

Poiché Chrome ha introdotto externally_connectable, questo è abbastanza facile da fare in Chrome. In primo luogo, specificare il dominio consentita nel manifest.json del file:

"externally_connectable": { 
    "matches": ["*://*.example.com/*"] 
} 

Usa chrome.runtime.sendMessage per inviare un messaggio dalla pagina:

chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url}, 
    function(response) { 
    // ... 
    }); 

Infine, ascoltare nella vostra pagina di sfondo con chrome.runtime.onMessageExternal:

chrome.runtime.onMessageExternal.addListener(
    function(request, sender, sendResponse) { 
    // verify `sender.url`, read `request` object, reply with `sednResponse(...)`... 
    }); 

Se non si ha accesso a externally_connectable supporto, la risposta originale segue:

Risponderò da una prospettiva Chrome-centric, anche se i principi descritti qui (iniezioni di script di pagine Web, script di background a esecuzione prolungata, passaggio di messaggi) sono applicabili praticamente a tutti i framework di estensione del browser .

Da un livello elevato, ciò che si vuole fare è iniettare uno content script in ogni pagina Web, che aggiunge un'API, accessibile alla pagina web. Quando il sito chiama l'API, l'API attiva lo script di contenuto per fare qualcosa, come inviare messaggi alla pagina di sfondo e/o inviare un risultato allo script di contenuto, tramite callback asincrono.

La principale difficoltà in questo caso è che gli script di contenuto "immessi" in una pagina Web non possono modificare direttamente il codice JavaScript execution environment di una pagina. Condividono il DOM, quindi gli eventi e nella struttura DOM sono condivisi tra lo script di contenuto e la pagina Web, ma le funzioni e le variabili non sono condivise. Esempi:

  • DOM manipolazione: Se uno script contenuto aggiunge un elemento <div> a una pagina, che funzionerà come previsto. Sia lo script di contenuto che la pagina vedranno il nuovo <div>.

  • Eventi: Se uno script contenuti imposta un listener di eventi, per esempio, per i clic su un elemento, l'ascoltatore si attiveranno con successo quando si verifica l'evento. Se la pagina imposta un listener per gli eventi personalizzati attivati ​​dallo script di contenuto, questi verranno ricevuti correttamente quando lo script di contenuto attiva quegli eventi.

  • Funzioni: Se lo script contenuti definisce una nuova funzione globale foo() (come si potrebbe provare quando la creazione di una nuova API). La pagina non può vedere o eseguire foo, perché foo esiste solo nell'ambiente di esecuzione dello script di contenuto, non nell'ambiente della pagina.

Quindi, come è possibile impostare un'API corretta? La risposta arriva in molti passaggi:

  1. Ad un basso livello, rendere il vostro API event-based. La pagina Web attiva eventi DOM personalizzati con dispatchEvent e gli script di contenuto li ascoltano con addEventListener, intervenendo quando vengono ricevuti. Ecco un semplice storage API basata su eventi che una pagina web può utilizzare per avere l'estensione per memorizzare i dati per esso:

    content_script.js (in proprio interno):

    // an object used to store things passed in from the API 
    internalStorage = {}; 
    
    // listen for myStoreEvent fired from the page with key/value pair data 
    document.addEventListener('myStoreEvent', function(event) { 
        var dataFromPage = event.detail; 
        internalStorage[dataFromPage.key] = dataFromPage.value 
    }); 
    

    non estensione pagina web, utilizzando la vostra API basata sugli eventi:

    function sendDataToExtension(key, value) { 
        var dataObj = {"key":key, "value":value}; 
        var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj}); 
        document.dispatchEvent(storeEvent); 
    } 
    sendDataToExtension("hello", "world"); 
    

    Come si può vedere, la pagina web ordinario sta sparando gli eventi che lo script contenuto può vedere e reagire, perché condividono t lui DOM. Gli eventi hanno dati allegati, aggiunti nello CustomEvent constructor. Il mio esempio qui è pietosamente semplice - puoi ovviamente fare molto di più nello script di contenuto una volta che ha i dati dalla pagina (molto probabilmente pass it allo background page per ulteriori elaborazioni).

  2. Tuttavia, questa è solo metà della battaglia. Nel mio esempio sopra, la pagina web ordinaria ha dovuto creare sendDataToExtension stesso. La creazione e l'attivazione di eventi personalizzati è abbastanza dettagliata (il mio codice occupa 3 righe ed è relativamente breve). Non vuoi forzare un sito a scrivere codice di attivazione eventi arcano solo per utilizzare la tua API. La soluzione è un po 'sgradevole: aggiungi un tag <script> al tuo DOM condiviso che aggiunge il codice di attivazione eventi all'ambiente di esecuzione della pagina principale.

    Interno content_script.js:

    // inject a script from the extension's files 
    // into the execution environment of the main page 
    var s = document.createElement('script'); 
    s.src = chrome.extension.getURL("myapi.js"); 
    document.documentElement.appendChild(s); 
    

    Le funzioni che sono definite in myapi.js diventeranno accessibili alla pagina principale. (Se si utilizza "manifest_version":2, è necessario includere myapi.js nell'elenco dei manifest di web_accessible_resources).

    myapi.JS:

    function sendDataToExtension(key, value) { 
        var dataObj = {"key":key, "value":value}; 
        var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj}); 
        document.dispatchEvent(storeEvent); 
    } 
    

    Ora il pianura pagina web può semplicemente fare:

    sendDataToExtension("hello", "world"); 
    
  3. C'è un ulteriore ruga al nostro processo API: lo script myapi.js non sarà disponibile esattamente tempo di caricamento Invece, verrà caricato un po 'di tempo dopo il caricamento della pagina. Pertanto, la semplice pagina web deve sapere quando è possibile chiamare in sicurezza la propria API. Puoi risolvere questo problema facendo sì che myapi.js attivi un evento "API pronto", che la tua pagina ascolta.

    myapi.js:

    function sendDataToExtension(key, value) { 
        // as above 
    } 
    
    // since this script is running, myapi.js has loaded, so let the page know 
    var customAPILoaded = new CustomEvent('customAPILoaded'); 
    document.dispatchEvent(customAPILoaded); 
    

    Plain pagina web utilizzando API:

    document.addEventListener('customAPILoaded', function() { 
        sendDataToExtension("hello", "world"); 
        // all API interaction goes in here, now that the API is loaded... 
    }); 
    
  4. Un'altra soluzione al problema della disponibilità di script in fase di carico è l'impostazione run_at proprietà di contenuti script in manifest a "document_start" come questo:

    manifest.json:

    "content_scripts": [ 
         { 
         "matches": ["https://example.com/*"], 
         "js": [ 
          "myapi.js" 
         ], 
         "run_at": "document_start" 
         } 
        ], 
    

    Estratto dal docs:

    Nel caso di "document_start", i file vengono iniettati dopo qualsiasi file da css, ma prima di ogni altra DOM è costruito o qualsiasi altro script viene eseguito.

    Per alcuni contenuti che potrebbero essere più appropriati e di minore impegno rispetto all'evento "A carico dell'API".

  5. Per inviare i risultati indietro alla pagina, è necessario fornire una funzione di richiamata asincrona. Non esiste alcun modo per restituire in modo sincrono un risultato dall'API, poiché l'attivazione/l'ascolto di eventi è intrinsecamente asincrono (ad esempio, la funzione dell'API del sito termina prima che lo script di contenuto ottenga l'evento con la richiesta dell'API).

    myapi.js:

    function getDataFromExtension(key, callback) { 
        var reqId = Math.random().toString(); // unique ID for this request 
        var dataObj = {"key":key, "reqId":reqId}; 
        var fetchEvent = new CustomEvent('myFetchEvent', {"detail":dataObj}); 
        document.dispatchEvent(fetchEvent); 
    
        // get ready for a reply from the content script 
        document.addEventListener('fetchResponse', function respListener(event) { 
         var data = event.detail; 
    
         // check if this response is for this request 
         if(data.reqId == reqId) { 
          callback(data.value); 
          document.removeEventListener('fetchResponse', respListener); 
         } 
        } 
    } 
    

    content_script.js (in proprio interno):

    // listen for myFetchEvent fired from the page with key 
    // then fire a fetchResponse event with the reply 
    document.addEventListener('myStoreEvent', function(event) { 
        var dataFromPage = event.detail; 
        var responseData = {"value":internalStorage[dataFromPage.key], "reqId":data.reqId}; 
        var fetchResponse = new CustomEvent('fetchResponse', {"detail":responseData}); 
        document.dispatchEvent(fetchResponse); 
    }); 
    

    ordinaria pagina web:

    document.addEventListener('customAPILoaded', function() { 
        getDataFromExtension("hello", function(val) { 
         alert("extension says " + val); 
        }); 
    }); 
    

    Il reqId è necessario nel caso in cui si abbiano richieste multiple contemporaneamente, in modo che non leggano le risposte sbagliate.

E penso che sia tutto!Quindi, non per i deboli di cuore, e forse non ne vale la pena, se consideri che altre estensioni possono anche legare gli ascoltatori ai tuoi eventi per intercettare come una pagina sta usando la tua API. So tutto questo solo perché ho realizzato un'API di crittografia proof-of-concept per un progetto scolastico (e successivamente ho appreso le principali insidie ​​di sicurezza ad esso associate).

In somma: Uno script di contenuto può ascoltare eventi personalizzati da una normale pagina Web e lo script può anche iniettare un file di script con funzioni che rendono più semplice per le pagine Web attivare tali eventi. Lo script di contenuto può passare i messaggi a una pagina di sfondo, che quindi memorizza, trasforma o trasmette i dati dal messaggio.

+1

Grazie per le spiegazioni dettagliate! +1 per indicare l'ambiente di esecuzione. – keewooi

+1

Considerare l'aggiornamento della risposta con il costruttore 'CustomEvent'. La sua sintassi sembra molto più cara del metodo deprecato 'document.createEvent'. –

+0

C'è un modo per controllare i dettagli di sicurezza della pagina/lo stato dell'HSTS prima di iniettare lo script? – r3m0t