Jak stworzyć dodatek do FireFox - krótki tutorial FF addons (extensions)



W jednym z ostatnich postów przedstawiłem kilka, moim zdaniem, wartościowych dodatków do FireFox pomocnych każdemu programiście www. Nadeszła pora, aby spróbować napisać własny dodatek. W poście tym opiszę jak stworzyć proste rozszerzenie do FF. Będzie to krótki i uproszczony tutorial, który mam nadzieję będzie pierwszym krokiem do tworzenia bardziej zaawansowanych i ciekawych dodatków.

 

 

   

Technologie

Jeżeli tworzyłeś już wcześniej strony oparte na DHTML prawdopodobnie bardzo szybko zrozumiesz ideę tworzenia rozszerzeń do FireFox. Wiedza którą posiadasz z dziedziny znanych już wcześniej technologii wymaga jedynie przestawienia się na inny "framework", jakim jest środowisko FireFox. Rozszerzenia FireFox opierają się na 4 poniższych technologiach i warstwach:

 

XPCOM (Cross Platform Component Object Model)

XPCOM to crossplatformowy model komponentów podobny do rozwiązania Microsoft COM. Model ten może być używany i implementowany w wielu językach, w tym JavaScript - języku wykorzystywanym przez dodatki FireFox. Ogólnie XPCOM oferuje zestaw podstawowych komponentów i klas m.in. do obsługi plików, zarządzania pamięcią, zarządzania wątkami, ... W przypadku XPCOM FireFox większość komponentów jest częścią silnika Gecko.

Innymi słowy XPCOM to warstwa umożliwiająca dostęp do operacji, które nie są możliwe bezpośrednio z pozycji JavaScript. Przykładem jest chociażby dostęp do plików. Łącznikiem pomiędzy JavaScript a XPCOM jest interfejs  XPConnect.

 

XUL

XUL jest językiem opisu interfejsu użytkownika opartym na język XML. XUL posiada zbiór podstawowych kontrolek i widgetów używanych w aplikacjach (przyciski, pulldowny, menu, ...). Najprostszym sposobem, aby przekonać się czym dokładnie jest XUL będzie wpisanie w polu adresu przeglądarki:

chrome://browser/content/browser.xul


Proszę bardzo, mamy przeglądarkę w przeglądarce! Teraz wystarczy otworzyć Firebug i skorzystać z funkcji inspect. Mam nadzieję, że jest to dość obrazowe wyjaśnienie roli jaką pełni XUL w FireFox :-)

 

JavaScript

Javascript jest warstwą logiki rozszerzeń i łącznikiem z XPCOM i XUL (dzięki DOM).

 

CSS

Warstwa opisu prezentacji XUL. 

 

Środowisko developerskie

Aby móc efektywnie pisać dodatki do FF musimy przygotować odpowiednie środowisko developerskie. W pierwszej kolejności należy stworzyć nowy profil FireFoxa. W tym celu tworzymy nowy skrót:

firefox.exe -no-remote -P dev

Po jego uruchomieniu powinno pojawić się okno zarządzania profilami przeglądarki. Założymy tam nowy profil 'dev'.
Po uruchomieniu nowego profilu wpisujemy w oknie adesu about:config i zmieniamy konfigurację następujących zmiennych, które będą przydatne przy testowaniu pisanych rozszerzeń:

nglayout.debug.disable_xul_cache - true
browser.dom.window.dump.enabled  - true
javascript.options.showInConsole - true
javascript.options.strict - true

Jeżeli któraś z powyższych zmiennych nie istnieje, należy ją dodać naciskając prawy przycisk myszy i wybierając Dodaj ustawienie typu -> Wartość logiczna (Boolean)

Warto również zainstalować bardzo pomocne dodatki: Firebug oraz Chromebug. Chromebug to narzędzie napisane na bazie Firebug pracujące na warstwie chrome XUL, czyli warstwie niższej od tej, na której pracuje Firebug. Po instalacji dodatku Chromebug należy uruchomić FireFox z dodatkowym parametrem:

firefox.exe -no-remote -P dev -chromebug

 

Struktura plików i katalogów rozszerzeń FireFox

Po dodaniu i skonfigurowaniu nowego profilu w FireFox możemy już rozpocząć pracę nad własnym dodatkiem. Nowo dodany profil będzie domyślnie dostępny w podobnej ścieżce:

C:\Documents and Settings\nazwa_uzytkownika\Dane aplikacji\Mozilla\Firefox\Profiles\mhawpb4j.dev\

Tam też utworzymy nowy katalog, w którym znajdzie się nasz dodatek:

extensions\moj_dodatek@dowolna_najlepiej_wlasna_domena.com

Przyjęło się, że nazwa katalogu (również ID dodatku) ma postać adresu e-mail. Jest to jednak rzecz umowna, możemy go nazwać zupełnie dowolnie.

Wewnątrz nowo utworzonego katalogu należy stworzyć odpowiednią strukturę plików oraz podkatalogów:

install.rdf
chrome.manifest
content/
locale/
skin/

Pliki oraz ich strukturę opiszę poniżej. Warto zwrócić uwagę na trzy powyższe katalogi tzw. pakietów:

  • content - Zawiera pliki XUL oraz JavaScript.
  • locale - Tutaj znajdują się pliki językowe interfejsu.
  • skin - Pliki związane z wizualizacją GUI, takie jak pliki CSS i obrazki.

 

Budujemy dodatek

W tutorialu tym spróbujemy zbudować dodatek, który będzie zapisywał w pliku tekstowym wyszukiwane w Google wyrażenia oraz ich datę. Nazwiemy go GoogleQueryLog. Dodatek bardzo prosty, ale może przy okazji komuś się przyda :-).

Do zrobienia mamy następujące elementy:

  • Warstwa logiki (JavaScript) odpowiedzialna za zbieranie informacji o wyszukiwanych wyrażeniach. Będzie ona realizowała:
    • przechwycenie zdarzenia zmiany adresu w polu adresu przeglądarki
    • sprawdzenie czy adres URL odpowiada wyrażeniu regularnemu odpowiadającemu URI wyszukiwania w google
    • jeżeli adres będzie pasował do wzorca zostanie z niego pobrane wyszukiwane wyrażenie
    • wyrażenie zostanie zapisane w pliku tekstowym wraz z datą
  • Nowa pozycja w menu przeglądarki Google Query Log -> Zobacz logi
  • Okno wyświetlające logi z pliku tekstowego

 

W katalogu extension profilu 'dev' FireFox zakładamy katalog: googlequerylog@moje.testy. Budujemy w nich opisaną powyżej strukturę katalogów i plików.

 

chrome.manifest

Plik chrome.manifest rejestruje pakiet dodatku chrome w FireFox. Zawartość pliku chrome.manifest będzie następująca:

content googlequerylog content/
skin googlequerylog classic/1.0 skin/classic/
locale googlequerylog en-US locale/en-US/
locale googlequerylog pl-PL locale/pl-PL/
overlay chrome://browser/content/browser.xul chrome://googlequerylog/content/overlay.xul

Pierwsza linia rejestruje pakiet typu content. googlequerylog to nazwa pakietu, content/ to relatywna ścieżka do folderu gdzie będą znajdować się pliki źródłowe. W sposób analogiczny definiowane są pozostałe pakiety: skin i locale. Nasz dodatek będzie dostępny w dwóch wersjach językowych: angielskiej(US) i polskiej. Dla pozostałych wersji językowych FF domyślnym językiem interfejsu dodatku będzie en-US.

Ostatnia linia określa tzn overlay. Overlay to technika pozwalająca na nałożenie na warstwę dokumentu XUL kolejnej warstwy. Wyżej podałem przykład wpisania adresu chrome://browser/content/browser.xul w polu adresu przeglądarki. browser.xul to dokument XUL okna przeglądarki FireFox. Na tą warstwę nałożymy własny dokument XUL. Potrzebne będzie nam to do tego, aby osadzić dodatkowo menu w przeglądarce opisane w założeniach naszego przykładowego dodatku.

 

install.rdf

Jest to dokument XML zawierający dane dotyczące dodatku, które są wymagane do jego instalacji w FireFox. Dokument ten będzie miał postać:

<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
  <Description about="urn:mozilla:install-manifest">
    <!-- Unikalny identyfikator dodatku --> 
    <em:id>googlequerylog@moje.testy</em:id>
    <!-- Informacje, że jest to dodatek do FF -->
    <em:type>2</em:type>
    <!-- Nazwa dodatku która będzie wyświetlana w menadżerze dodatków FF -->
    <em:name>Google Query Log</em:name>
    <!-- Wersja dodatku -->
    <em:version>0.0.1</em:version>
    <!-- Opis który będzie widoczny w menadżerze dodatków FF -->
    <em:description></em:description>
    <!-- Autor -->
    <em:creator>mturek</em:creator>
    <!-- Strona dodatku -->
    <em:homepageURL>http://moje.testy/</em:homepageURL>
    <em:targetApplication>
      <Description>
        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
        <!-- Minimalna wersja FF którą obsługuje dodatek -->
        <em:minVersion>3.0</em:minVersion>
        <!-- Maksymalna wersja FF którą obsługuje dodatek -->
        <em:maxVersion>3.6.*</em:maxVersion>
      </Description>
    </em:targetApplication>
  </Description>
</RDF>

 

content/overlay.xul

Plik overlay.xul umieścimy w katalogu content, czyli wskazanym w pliku chrome.manifest:

<?xml version="1.0"?>
<!DOCTYPE window SYSTEM "chrome://googlequerylog/locale/qooglequerylog.dtd">
<overlay id="googlequerylogOverlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
    <script type="application/javascript" src="chrome://googlequerylog/content/service.js"/>
    <menubar id="main-menubar">
        <menu id="googlequerylog-menu" label="Google Query Log" insertafter="helpMenu">
            <menupopup id="sitehoover-menupopup">
                <menuitem id="sitehoover-menu-showlog" label="&overlay.menuShowLogs;" oncommand="Service.dialogLogOpen()" />
            </menupopup>
        </menu>
    </menubar>  
</overlay>

Teraz pora uruchomić developerski profil FireFoxa.

Powinniśmy w menu zobaczyć nową pozycję:

Wytłumaczę w jaki sposób na podstawie powyższego pliku XUL została dodana nowa pozycja menu. Używając Chromebug skorzystaj z funkcji inspect i najedź myszą na górną belkę menu. W DOM zauważysz, że w głównym dokumenci XUL przeglądarki chrome://browser/content/browser.xul jest obiekt o ID main-menubar. Ten sam ID jest użyty w naszym dokumencie XUL. FireFox uruchamiając nasz dodatek połączył oba dokumenty wiedząc do którego obiektu ma się "przykleić". W kolejnym tagu naszego XUL (menu) jest atrybut insertafter który ma wartość helpMenu. Jest ID elementu menu "Pomoc" ...


Prawda, że proste? :-)

W naszym dokumencie XUL dołączony jest plik chrome://googlequerylog/content/service.js, czyli nasza lokalna biblioteka skryptu JS, która za chwilę stworzymy. Będzie ona zawierała metody potrzebne do realizacji naszego zadania.

 

content/service.js

Pora zabrać się za logikę naszej aplikacji, czyli skrypt JavaScript obsługujący nasz dodatek. Plik umieścimy również w katalogu content (analogiczna ścieżka, jak ta określona w stworzonym dokumencie XUL).

try {
    netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
} catch (e) {
    alert("Dostęp do pliku niedozwolony (FF).");
}

window.addEventListener('load', function () {
        gBrowser.addEventListener('DOMContentLoaded', function () {
            Service.parseUrl(gBrowser.currentURI.spec);
        }, false)
    }, false);


var Service = {
    extensionId: 'googlequerylog@moje.testy',
    logFilename: 'qooglequerylog.log',    
    query: '',
    logFile: null,
    
    dialogLogOpen: function(){
        window.openDialog('chrome://googlequerylog/content/showlogs.xul','','chrome,centerscreen,modal');
    },
    
    dialogLogInit: function(){
        var logContent = Service.getQueryLog();
        document.getElementById('googlequerylog-textbox').value = logContent;
    },
    
    parseUrl: function(_url){
        reg = new RegExp(/^http:\/\/www.google.(pl|com)\/.*(&|\?)q=(.*?)&/);
        reg_dest = reg.exec(_url);
        if (reg_dest != null){
            var query = decodeURIComponent(reg_dest[1].replace(/\+/g, '%20'));
            if (query != this.query){
                this.saveQueryLog(query);
            }
        }
        this.query = query;
    },
    
    fileInit: function(){
        var extension = Components.classes["@mozilla.org/extensions/manager;1"]
                                           .getService(Components.interfaces.nsIExtensionManager)
                                           .getInstallLocation(this.extensionId)
                                           .getItemLocation(this.extensionId);
        var filePath = extension.path + "\\" + this.logFilename;;
        this.logFile = Components.classes["@mozilla.org/file/local;1"]
                                      .createInstance(Components.interfaces.nsILocalFile);
        this.logFile.initWithPath(filePath);
        if(this.logFile.exists() == false){
            this.logFile.create( Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 420);
        }
    },
    
    saveQueryLog: function(_query){
        var now = new Date();
        var content = this.getDate() + '|' + _query + '\r\n';
        this.fileInit();
        var outputStream  = Components.classes["@mozilla.org/network/file-output-stream;1"]
                                                .createInstance(Components.interfaces.nsIFileOutputStream);
        var convertOutputStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
                                                    .createInstance(Components.interfaces.nsIConverterOutputStream);
        outputStream.init(this.logFile, 0x02 | 0x08 | 0x10, 0666, 0);
        
        // konwertujemy do UTF-8 i zapisujemy plik
        convertOutputStream.init(outputStream, "UTF-8", 0, 0);
        convertOutputStream.writeString(content);
        convertOutputStream.close();
    },
    
    getQueryLog: function(){
        this.fileInit();
        var inputStream  = Components.classes["@mozilla.org/network/file-input-stream;1"]
                                              .createInstance(Components.interfaces.nsIFileInputStream);
        var convertInputStream  = Components.classes["@mozilla.org/intl/converter-input-stream;1"]
                                                     .createInstance(Components.interfaces.nsIConverterInputStream);
        inputStream.init(this.logFile, 0x01, 00004, null);
        convertInputStream.init(inputStream, 'UTF-8', 1024, 0xFFFD);
        convertInputStream.QueryInterface(Components.interfaces.nsIUnicharLineInputStream);  
        var outputArray = new Array();
        if (convertInputStream instanceof Components.interfaces.nsIUnicharLineInputStream) {  
            var line = {};  
            var cont;  
            do {  
                cont = convertInputStream.readLine(line);  
                outputArray.push(line.value.split("|"));
            } while (cont);  
        }
        convertInputStream.close();
        outputArray.reverse();
        var output = '';
        for (var i = 0; i < outputArray.length; i++ ){
            if (outputArray[i][1]){
                output += outputArray[i][0] + ' - ' + outputArray[i][1] + "\r\n";
            }
        }
        return output;
    },
    
    getDate: function(){
        var d = new Date();
        function pad(n){
            return n<10 ? '0'+n : n
        }
        return d.getFullYear()+'-'
            + pad(d.getMonth()+1)+'-'
            + pad(d.getDate())+' '
            + pad(d.getHours())+':'
            + pad(d.getMinutes())+':'
            + pad(d.getSeconds());
    }
}


Na początku pliku dodajemy obsługę zdarzenia 'load' okna przeglądarki, które będzie wywoływało funkcję Service.parseUrl(). Argumentem tej funkcji jest adres otwieranej strony. Przekazywany adres URL będzie porównywany ze wzorcem odpowiadającym adresowi query wyszukiwania w Google i w przypadku dopasowania wzorca wyszukiwane wyrażenie zostanie zapisane w pliku.  Aby uzyskać dostęp do lokalnego systemu plików użyte zostały komponenty XPCOM. Za zapis logów odpowiada funkcja Service.saveQueryLog().

 

content/showlogs.xul

W dokumencie overlay.xul w elemencie menuitem dodana jest obsługa zdarzenia oncommand. Jest to zdarzenie, które zostanie wywołane po wybraniu myszką pozycji menu "Pokaż logi" - czyli wywołanie metody Service.dialogLogOpen(). Metoda ta otwiera w nowym oknie dokument showlogs.xul:

<?xml version="1.0"?>  
<?xml-stylesheet href="chrome://global/skin/"?>
<?xml-stylesheet href="chrome://googlequerylog/skin/style.css"?>
<!DOCTYPE window SYSTEM "chrome://googlequerylog/locale/qooglequerylog.dtd">
<window id="googlequerylog-logs-dialog" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"  
        title="&logswindow.title;"
        onload="Service.dialogLogInit();">  
    <script type="application/javascript" src="chrome://googlequerylog/content/service.js"/>  
    <box height="400" width="400">
        <textbox id="googlequerylog-textbox" flex="1" multiline="true" readonly="true" />
    </box>
    <button label="&logswindow.closButton;" oncommand="window.close();" />
</window>  

Zwróć uwagę na zdarzenie onload na elemencie window. Fukcja Service.dialogLogInit() uruchamia sekwencje otwarcia pliku logów z dysku oraz wyświetlenie ich w nowym oknie.

 

style.css

W chrome.manifest zarejestrowaliśmy pakiet skin. Jest to pakiet, w którym definiujemy warstwę prezentacji, która reprezentowana jest arkuszem CSS. W naszym dodatku równie dobrze mogłoby jej nie być, jednak dodałem ją, aby zaprezentować w jaki sposób jest to realizowane w dodatkach do FireFox. Z arkusza css korzysta showlogs.xul, gdzie jest on dołączany w prologu dokumentu:

<?xml-stylesheet href="chrome://googlequerylog/skin/style.css"?>

Plik style.css umieszczony jest w lokalizacji skin/classic/style.css

#googlequerylog-textbox {
    -moz-appearance: none;
    background:#FFF8D6;
}

Styl ten zmienia kolor tła elementu texbox. Mozilla wprowadza wiele dodatkowych styli, które możemy użyć w ramach rozszerzeń. Opis wszystkich znajduje się tutaj. W przykładzie powyżej znajduje się jeden z nich: -moz-appearance:none, który wyłącza natywne style textboxa. Bez użycia tego stylu nie bylibyśmy w stanie zmienić tła tego elementu.

Warto zwrócić uwagę również na dodany arkusz chrome://global/skin/. Jest to arkusz globalny w którym zdefiniowane są style globalne przeglądarki (np. wizualizacja otwieranego okna, przycisków, ...).

 

Wersje językowe

Powróćmy ponownie do pliku chrome.manifest. Zdefiniowaliśmy w nim wpisy odpowiadające deklaracji wersji językowych dodatku:

locale googlequerylog en-US locale/en-US/
locale googlequerylog pl-PL locale/pl-PL/

W dodatku mamy trzy elementy, które należałoby przetłumaczyć na inne języki. Jest to pozycja w menu "Pokaż logi" oraz nazwa okna z logami i przycisk "Zamknij". Są one umieszczone w dokumentach XUL. Dokumenty XUL mają składnie XML, więc skorzystamy z encji, które będą zwierały tłumaczenia.

Encję dołączamy deklaracją DOCTYPE

<!DOCTYPE window SYSTEM "chrome://googlequerylog/locale/qooglequerylog.dtd">

Zgonie z chrome.manifest definicje encji umieścimy w lokalizacjach /locale/pl-PL/qooglequerylog.dtd oraz analogicznie dla wersji en-US:

<!ENTITY overlay.menuShowLogs "Pokaż logi">
<!ENTITY logswindow.title "Logi">
<!ENTITY logswindow.closButton "Zamknij"> 

W dokumencie XUL będziemy się do nich odwoływać za pomocą encji, np:
&overlay.menuShowLogs;
Są one umieszczone w plikach overlay.xul i showlogs.xul.
Można zauważyć, że w deklaracji ścieżki DOCTYPE nie są uwzględniane katalogi językowe, np. pl-PL. Silnik FireFox automatycznie je uzupełnia w zależności od wersji językowej przeglądarki.

Dobrze, ale w jaki sposób skorzystać z etykiet językowych w JavaScript? Akurat w naszym przykładzie nie było takiej potrzeby, jednak warto przy okazji o tym wspomnieć.
Przykładowo stworzymy plik locale/pl-PL/qooglequerylog.properties

googlequerylog.label1=etykieta 1
googlequerylog.label2=etykieta 2

Poniżej znajduje się przykładowy kod JavaScript pokazujący w jaki sposób możemy skorzystać z tak zdefiniowanych etykiet:

var _bundle = gmyextensionBundle.createBundle("chrome://googlequerylog/locale/googlequerylog.properties");
alert(_bundle.GetStringFromName('googlequerylog.label1'));

 

Testy

Chciałbym wspomnieć o dość ważnej kwestii związanej z testowaniem pisanych dodatków. Wyżej przedstawiłem konfigurację stworzonego nowego profilu FireFox do celów deweloperskich. W konfiguracji tej zostało m.in. wyłączone cacheowanie ładowanych dokumentów XUL. Jeżeli dodatek opiera się na niezależnym dokumencie XUL (jak w naszym przypadku okno z logami) jego testowanie nie wymaga każdorazowego restartu FireFox. Jeżeli jednak testujemy warstwę overlay, która jest nałożona na główne okno przeglądarki (chrome://browser/content/browser.xul) konieczne będzie cykliczne zamykanie i ponowne otwieranie FF.

  

Paczka dystrybucyjna - XPI

Ostatnim elementem jest przygotowanie dystrybucji dodatku w formie plików XPI. XPI jest niczym innym jak archiwum ZIP. Paczka XPI zawiera lekko zmodyfikowaną strukturę katalogów i plików w stosunku do wersji deweloperskiej:

chrome.manifest
install.rdf
chrome/googlequerylog.jar

.jar to również plik ZIP. Do przygotowania paczki archiwów polecam paker 7-Zip.

Katalogi content, locale, skin zarchiwizujemy do formatu ZIP i zmienimy nazwę stworzonego archiwum na googlequerylog.jar. Umieścimy go w katalogu chrome (zgodnie z powyższa strukturą paczki XPI).

Następnie zmodyfikujemy plik chrome.manifest, tak aby miał postać:

content googlequerylog jar:chrome/googlequerylog.jar!/content/
skin googlequerylog classic/1.0 jar:chrome/googlequerylog.jar!/skin/classic/
locale googlequerylog en-US jar:chrome/googlequerylog.jar!/locale/en-US/
locale googlequerylog pl-PL jar:chrome/googlequerylog.jar!/locale/pl-PL/
overlay chrome://browser/content/browser.xul chrome://googlequerylog/content/overlay.xul

Widzimy, że dodaliśmy informacje, że poruszamy się względem archiwum .jar.

Teraz całą strukturę plików i katalogów zarchiwizujemy do ZIP i zmienimy nazwę archiwum na googlequerylog.xpi.

To wszystko. Na koniec wystarczy przeciągnąć stworzony plik XPI do okna FireFox aby go zainstalować.

Powyższy dodatek można pobrać stąd *

 

Na koniec

Tutorial ten jest jedynie wprowadzeniem do tematu rozszerzeń Mozilli. Możliwości jakie daje nam FireFox do tworzenia dodatków są naprawdę duże, co zresztą widać po bogatej bibliotece dostępnych rozszerzeń. W wielu przypadkach są to bardzo użyteczne dodatki, bez których często nie można sobie już wyobrazić korzystania z przeglądarki. Jak widać, aby napisać własny dodatek wystarczy jedynie dobry pomysł, bo samo jego stworzenie, mam nadzieję, nie wydaje się już takie trudne. Powodzenia!

 

Przydatne linki

 


* Powyższy dodatek został stworzony do celów tego krótkiego kursu. Dodatek jest uproszczony i niestety niezbyt wydajny. Przykładem jest okno pokazujące logi. Są one prezentowane w odwrotnej kolejności w stosunku do zapisanychw pliku. Jest to wykonane na obiekcie typu array, więc przy plikach o większych rozmiarach jest to nieoptymalne. Może warto przy okazji ćwiczeń zoptymalizować ten fragment? :-)

 

 



Proszę czekać...
Nie możesz komentować. Mariusz Turek umieścił Cię na czarnej liście lub Twoje konto nie jest aktywne.