0.92/components/notify.js

Back to the directory /*
    --------------------------------------------------------
    Notify
    --------------------------------------------------------
    
    Copyright (C) 2006, Gemme.pl
    http://notify.torino.pl/en/
    All rights reserved.
    
    See attached file: LICENCE.TXT
    
    --------------------------------------------------------
*/

// #################################################################
//                             Preferences
// #################################################################

function NotifySitesPreferences() {
    
    this.parent = null; // CNotify object
    this.prefs = null;
    this.PrefService = null;
    
    this.isSiteAlreadyObserved = function(aURL)
    {
        for (var el in this.parent.ObservedSites)
        {
            if (aURL == this.parent.ObservedSites[el].URL ||
                aURL + "/" == this.parent.ObservedSites[el].URL ||
                aURL == this.parent.ObservedSites[el].URL + "/") // users type "http://foo.com" instead of "http://foo.com/"
                return true;
        }
        return false;
    }
    
    this.addObservedSite = function(aURL)
    // returns false if site already exists
    {
        if (this.isSiteAlreadyObserved(aURL))
            return false;
        
        var s = new NotifyWatchSite();
        s.URL = aURL;
        s.LastChange = new Date();
        s.ServerIsKind = false; // TODO: check, use HTTP 1.1
        
        var sites = this.prefs.getCharPref("observedSites");
        if (sites != "") {
            sites += "|" + s.toString();
        } else {
            sites = s.toString();
        }
        
        this.prefs.setCharPref("observedSites", sites);
        this.PrefService.savePrefFile(null); // save to disk
        
        return true;
        
        // NotifyPreferences.refreshSites() should go now
    }
    
    this.addCalendarSite = function(aURL, aYear, aMonth, aDay)
    // aMonth is 1..12
    {
        var s = new NotifyCalendarSite();
        s.URL = aURL;
        var d = new Date(aYear, aMonth - 1, aDay, 0, 0, 0, 0);
        s.When = d.getTime();
        s.Opened = false;
        
        var sites = this.prefs.getCharPref("calendarSites");
        if (sites != "") {
            sites += "|" + s.toString();
        } else {
            sites = s.toString();
        }
        
        this.prefs.setCharPref("calendarSites", sites);
        this.PrefService.savePrefFile(null); // save to disk
        
        return true;
        
        // NotifyPreferences.refreshSites() should go now
    }
    
    this.deleteObservedSite = function(x, saveAfterDelete)
    {
        delete this.parent.ObservedSites[x];
        
        if (saveAfterDelete)
        {
            this.saveObservedSites();
        }
    }
    
    this.deleteCalendarSite = function(x, saveAfterDelete)
    {
        delete this.parent.CalendarSites[x];
        
        if (saveAfterDelete)
        {
            this.saveCalendarSites();
        }
    }
    
    this.saveObservedSites = function()
    {
        // this.parent.ObservedSites.join("|") sucks!
        var sites = "";
        for (var el in this.parent.ObservedSites) {
            sites += this.parent.ObservedSites[el].toString() + "|";
        }
        if (sites.length) {
            sites = sites.substr(0, sites.length-1); // last "|"
        }
        
        this.parent.Prefs.reloadOnRefresh = false; // there's no need to refresh this.parent.ObservedSites
                                                   // TODO: does it work?
        try    {
            this.prefs.setCharPref("observedSites", sites);
            this.PrefService.savePrefFile(null); // save to disk
        }
        finally {
            this.parent.Prefs.reloadOnRefresh = true;
        }
    }
    
    this.saveCalendarSites = function()
    {
        // this.parent.CalendarSites.join("|") sucks!
        var sites = "";
        for (var el in this.parent.CalendarSites) {
            sites += this.parent.CalendarSites[el].toString() + "|";
        }
        if (sites.length) {
            sites = sites.substr(0, sites.length-1); // last "|"
        }
        
        this.parent.Prefs.reloadOnRefresh = false; // there's no need to refresh this.parent.ObservedSites
                                                   // TODO: does it work?
        try    {
            this.prefs.setCharPref("calendarSites", sites);
            this.PrefService.savePrefFile(null); // save to disk
        }
        finally {
            this.parent.Prefs.reloadOnRefresh = true;
        }
    }
    
} // NotifySitesPreferences

function NotifyPreferences() {

    this.prefs = null;
    this.PrefService = null;
    this.parent = null; // CNotify object
    this.sites = null;
    this.reloadOnRefresh = true; // - is there need to recreate this.parent.ObservedSites and this.parent.CalendarSites?
    this.openInTab = false;
    this.displayLastModified = false;
    
    // some preference in our branch has changed
    this.observe = function(subject, topic, data)
    {
        if (topic != "nsPref:changed") {
            return;
        }
        
        switch(data)
        {
            case "calendarSites":
            case "observedSites": // TODO: test
                this.refreshSites();
                break;
            case "sound":
            case "tray":
            case "open":
                this.refreshAlarms();
                break;
            case "period":
                this.refreshTimer();
                break;
            default:
                this.refreshOthers();
                break;
        }
    }
    
    this.refreshTimer = function()
    {
        this.parent.timerInterval = Math.max(1, this.prefs.getIntPref("period")) /* min */ * 60 * 1000;
        this.parent.setTimer();
    }
    
    this.refreshAlarms = function()
    {
        this.parent.Alarm.open = this.prefs.getBoolPref("open");
        this.parent.Alarm.tray = this.prefs.getBoolPref("tray");
        this.parent.Alarm.sound = this.prefs.getBoolPref("sound");
    }
    
    this.refreshSites = function()
    {
        // sites changed - reload
        
        if ( !this.reloadOnRefresh )
            return;
        
        delete this.parent.ObservedSites;
        delete this.parent.CalendarSites;
        
        this.parent.ObservedSites = new Array();
        this.parent.CalendarSites = new Array();
        
        
        var s = this.prefs.getCharPref("observedSites");
        var a = s.split("|");
        for (var el in a)
        {
            if (a[el] != "")
            {
                site = new NotifyWatchSite();
                site.loadFromString(a[el]);
                this.parent.ObservedSites.push(site);
            }
        }
        
        s = this.prefs.getCharPref("calendarSites");
        a = s.split("|");
        for (var el in a)
        {
            if (a[el] != "")
            {
                site = new NotifyCalendarSite();
                site.loadFromString(a[el]);
                this.parent.CalendarSites.push(site);
            }
        }
    }
    
    this.refreshOthers = function()
    {
        this.displayLastModified    = this.prefs.getBoolPref("displayLastModified");
        this.openInTab                = this.prefs.getBoolPref("openInTab");
    }
    
    this.init = function()
    {
        this.refreshAlarms();
        this.refreshSites();
        this.refreshTimer();
        this.refreshOthers();
    }
    
    // main constructor code
    this.PrefService =
        Components.classes["@mozilla.org/preferences-service;1"]
        .getService(Components.interfaces.nsIPrefService);
    this.prefs = 
        this.PrefService.getBranch("extensions.notify.");
    this.prefs.QueryInterface(Components.interfaces.nsIPrefBranch2);
    this.prefs.addObserver("", this, false);
    
    this.sites = new NotifySitesPreferences(this);
    this.sites.PrefService = this.PrefService;
    this.sites.prefs = this.prefs;
    
}; // NotifyPreferences


// #################################################################
//                                 Sites
// #################################################################

function NotifySite() {
    // Stores persistent info about a site.
    // Since NotifySite objects are recreated after every preferences change
    // (NotifyPreferences.refreshSites()), they cannot hold information
    // like opened window reference.
    
    this.URL = "";
    
    this.toString = function() // abstract
    {
        // result must not contain "|"
    }
    
    this.loadFromString = function(s) // abstract
    {
    }
    
}

function NotifyWatchSite() {
    
    this.URL = "";
    this.LastChange = null; // Date object
    this.ServerIsKind = false; // provides Last-Modified and ETag headers
    this.Hash = "";
    this.ETag = "";
    
    this.toString = function()
    {
        // <1,"url","date","Hash","ETag">
        //  1 - if ServerIsKind, otherwise 0
        var s =
            "<" +
            ((this.ServerIsKind == true) ? "1" : "0") +
            ",\"" +
            this.URL +
            "\",\"" +
            this.LastChange +
            "\",\"" +
            this.Hash +
            "\",\"" +
            this.ETag +
            "\">";
        return s;
    }
    this.loadFromString = function(s)
    {
        var URLEnd = s.indexOf("\"", 4);
        var LastChangeEnd = s.indexOf("\"", URLEnd + 3);
        var HashEnd = s.indexOf("\"", LastChangeEnd + 3);
        var ETagEnd = s.indexOf("\"", HashEnd + 3);
        
        this.ServerIsKind = (s[1] == "1");
        this.URL =
            s.substring(/* after <" */ 4, URLEnd);
        this.LastChange =
            new Date(s.substring(URLEnd + 3, LastChangeEnd));
        this.Hash =
            s.substring(LastChangeEnd + 3, HashEnd);
        this.ETag =
            s.substring(HashEnd + 3, ETagEnd);
    }
    
}
NotifyWatchSite.prototype = new NotifySite();

function NotifyCalendarSite() {
    
    this.URL = "";
    this.When = 0; // date.getTime()
    this.Opened = false;
    
    this.toString = function()
    {
        // <1,"url","date - getTime()">
        //  1 - if already opened, otherwise 0
        
        var s =
            "<" +
            ((this.Opened == true) ? "1" : "0") +
            ",\"" +
            this.URL +
            "\",\"" +
            this.When +
            "\">";
        return s;
    }
    this.loadFromString = function(s)
    {
        var URLEnd = s.indexOf("\"", 4);
        this.Opened = (s[1] == "1");
        this.URL =
            s.substring(/* after <1," */ 4, URLEnd);
        this.When =
            parseInt(s.substring(URLEnd + 3, s.indexOf("\"", URLEnd + 3)));
    }
    
}
NotifyCalendarSite.prototype = new NotifySite();

// #################################################################
//                                 Alarm
// #################################################################

function NotifyAlarm() {
    
    this.parent = null;
    
    this.open = true;
    this.tray = true;
    this.sound = true;
    
    this.openAndReuseOneTabPerURL = function(url)
    // modified code from:
    // http://kb.mozillazine.org/Reusing_tabs_for_the_same_URL
    {
        var wm =
            Components.classes["@mozilla.org/appshell/window-mediator;1"]
            .getService(Components.interfaces.nsIWindowMediator);
        var browserEnumerator =
            wm.getEnumerator("navigator:browser");

        // Check each browser instance for our URL
        var found = false;
        while (browserEnumerator.hasMoreElements() && !found)
        {
            var browserInstance = browserEnumerator.getNext();
            browserInstance = browserInstance.getBrowser();

            // Check each tab of this browser instance
            var index = 0, numTabs = browserInstance.mPanelContainer.childNodes.length;
            while (index < numTabs && !found)
            {
                var currentTab = browserInstance.getBrowserAtIndex(index);
                if (url == currentTab.currentURI.spec ||
                    url + "/" == currentTab.currentURI.spec) // users type "http://foo.com" instead of "http://foo.com/"
                {
                    // The URL is already opened. Select its tab.
                    browserInstance.selectedTab = currentTab;
                    // Focus *this* browser
                    browserInstance.focus();
                    found = true;
                }
                index++;
            }
        }

        // Our URL isn't open. Open it now.
        if (!found)
        {
            var recentWindow = wm.getMostRecentWindow("navigator:browser");
            if (recentWindow) {
                // Use an existing browser window
                recentWindow.delayedOpenTab(url);
            }
            else {
                // No browser windows are open, so open a new one.
                this.openURLinNewWindow(url);
            }
        }
    }
    
    this.openURLinNewWindow = function(aURL)
    {
        var url =
            Components.classes["@mozilla.org/supports-string;1"]
            .createInstance(Components.interfaces.nsISupportsString);
        url.data = aURL;
        
        var ww =
            Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
            .getService(Components.interfaces.nsIWindowWatcher);
        this.parent.OpenedWindowReferences[aURL] = ww.openWindow(null, "chrome://browser/content/browser.xul", "_blank",
            "chrome,all,dialog=no", url);
    }
    
    this.openSite = function(aURL)
    // opens in new tab/window
    {
        // http://developer.mozilla.org/en/docs/DOM:window.open#Best_practices
        
        if (this.parent.Prefs.openInTab)
        {
            // open in tab
            this.openAndReuseOneTabPerURL(aURL);
        }
        else
        {
            // open in window
            if (this.parent.OpenedWindowReferences[aURL] == null ||
                this.parent.OpenedWindowReferences[aURL].closed)
            {
                this.openURLinNewWindow(aURL);
            }
            else
            {
                this.parent.OpenedWindowReferences[aURL].focus(); // TODO: reload?
            }
        }
    }
    
    this.alertInTray = function(aURL, IsFromCalendar)
    {
        // http://developer.mozilla.org/en/docs/nsIAlertsService
        // http://lxr.mozilla.org/mozilla/source/toolkit/components/alerts/public/nsIAlertsService.idl
        
        // check if the alert service is present
        if (!("@mozilla.org/alerts-service;1" in Components.classes))
            return;
        
        var s;
        if (IsFromCalendar) {
            s = this.parent.StrBundle.GetStringFromName("TrayMessageTitleCalendar"); // TODO: user's message?
        }
        else
        {
            s = this.parent.StrBundle.GetStringFromName("TrayMessageTitleNotify");
        }
        
        var Alarm = this;
        var listener = {
            observe: function(subject, topic, data) {
                if (topic == 'alertclickcallback')
                {
                    Alarm.openSite(aURL);
                }
            }
        }
        
        var AlertService =
            Components.classes["@mozilla.org/alerts-service;1"]
            .getService(Components.interfaces.nsIAlertsService);
        
        AlertService.showAlertNotification(
            "chrome://notify/content/emblem-important.png", s, aURL, true, '', listener);
    }
    
    this.playSound = function()
    {
        const SoundFile = "Notify.wav";            // TODO: pref?
        const NotifyId  = "notify@torino.pl";
        
        var SoundPath =
            Components.classes["@mozilla.org/extensions/manager;1"]
            .getService(Components.interfaces.nsIExtensionManager)
            .getInstallLocation(NotifyId)
            .getItemLocation(NotifyId);
        SoundPath.append(SoundFile);
        
        var sound = Components.classes["@mozilla.org/sound;1"]
            .createInstance(Components.interfaces.nsISound);
        
        var filePath =
            new Components.Constructor(
                "@mozilla.org/file/local;1",
                "nsILocalFile", "initWithPath");
        var file = new filePath(SoundPath.path);
        var ioService =
            Components.classes['@mozilla.org/network/io-service;1']
            .getService(Components.interfaces.nsIIOService);
        var url = ioService.newFileURI(file, null, null);
        
        sound.play(url);
    }
    
    this.alarm = function(Site, AlwaysOpen, IsFromCalendar)
    {
        if (this.open || AlwaysOpen)
        {
            /* TODO: https? - solve this when adding new page
            if (Site.URL.toLowerCase().indexOf("http://") != 0)
            {
                Site.URL = "http://" + Site.URL;
            }
            */
            this.openSite(Site.URL);
        }
        if (this.tray)
        {
            this.alertInTray(Site.URL, IsFromCalendar);
        }
        if (this.sound)
        {
            this.playSound();
        }
    }
    
} // NotifyAlarm


// #################################################################
//                                 Main
// #################################################################

// interfaces we support
const nsINotify        = Components.interfaces.nsINotify;
const nsISupports    = Components.interfaces.nsISupports;
const nsITimer        = Components.interfaces.nsITimer;
const nsIObserver    = Components.interfaces.nsIObserver;

// class constants
const CLASS_ID = Components.ID("{12E355B4-EE49-11DA-9B41-B622A1EF5492}");
const CLASS_NAME = "Notify Extension Component";
const CONTRACT_ID = "@torino.pl/notify;1";
const OBSERVER_NAME = "Notify Extension Observer";

const OBSERVER_CONTRACT_ID = "@mozilla.org/observer-service;1";
const TIMER_CONTRACT_ID = "@mozilla.org/timer;1";
const HASH_CONTRACT_ID = "@mozilla.org/security/hash;1";
const nsICryptoHash = Components.interfaces.nsICryptoHash;

const BUNDLE = "chrome://notify/locale/notify.properties";


// class constructor
function CNotify()
{
    this.initService();
};

// class definition
CNotify.prototype = {
    
    StrBundle: null,
    Timer: null,
    OpenedWindowReferences: null,
    
    ObservedSites: null,
    CalendarSites: null,
    Alarm: null,
    Prefs: null,
    
    timerInterval: 15 /* min */ * 60 * 1000,
    // changed in this.Prefs.init(); anyway
    
    initService : function()
    {
        var obs = Components.classes["@mozilla.org/observer-service;1"]
            .getService(Components.interfaces.nsIObserverService);
        obs.addObserver(this, "profile-after-change", false);
    },
    
    init : function()
    {
        this.wrappedJSObject = this;
        
        var StrBundleService =
            Components.classes["@mozilla.org/intl/stringbundle;1"]
            .createInstance(Components.interfaces.nsIStringBundleService);
        this.StrBundle = StrBundleService.createBundle(BUNDLE);
        
        this.OpenedWindowReferences = new Object(); // associative array
        // see also: http://www.andrewdupont.net/2006/05/18/javascript-associative-arrays-considered-harmful/
        
        this.ObservedSites = new Array();
        this.CalendarSites = new Array();
        
        this.Alarm = new NotifyAlarm();
        this.Alarm.parent = this;
        
        this.Prefs = new NotifyPreferences();
        this.Prefs.parent = this;
        this.Prefs.sites.parent = this;
        this.Prefs.init();
        
        //this.Prompt = Components.classes["@mozilla.org/network/default-prompt;1"].createInstance(Components.interfaces.nsIPrompt);
        //this.Prompt.alert('test', 'start');
        
        this.setTimer();
        this.checkSites();
    },
    
    checkSites : function()
    {
        for (var el in this.ObservedSites)
        {
            var Request =
                Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
                .createInstance(Components.interfaces.nsIXMLHttpRequest);
            /*
                http://forums.mozillazine.org/viewtopic.php?t=427229
                http://www.mail-archive.com/dev-tech-xpcom@lists.mozilla.org/msg00264.html
                So we need this line:
            */
            Request instanceof Components.interfaces.nsIJSXMLHttpRequest;
            
            Request.open("GET", this.ObservedSites[el].URL, true);
            // TODO: HEAD, "the correct way"]
            
            var Notify = this;
            var func =
                eval(
                    "function(event) \
                    { \
                        Notify.siteChecked(event, " + el + "); \
                    };"
                    );
            Request.onload = func;
            
            // don't get a copy from cache
            Request.channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
            // don't slow down normal browser activity
            if (Request.channel instanceof Components.interfaces.nsISupportsPriority)
                Request.channel.priority = Components.interfaces.nsISupportsPriority.PRIORITY_LOWEST;
            // above 2 tricks thanks to this thread: http://forums.mozillazine.org/viewtopic.php?t=397699
            
            Request.send(null);
        }
        
        var nowDate = new Date();
        var now = nowDate.getTime();
        var changed = false;
        
        for (var el in this.CalendarSites)
        {
            if ( !this.CalendarSites[el].Opened && (now >= this.CalendarSites[el].When) )
            {
                this.Alarm.alarm(this.CalendarSites[el], true, true);
                
                // don't alarm about the site anymore
                this.CalendarSites[el].Opened = true;
                changed = true;
                
                // TODO: option - delete it?
            }
        }
        if (changed) {
            this.Prefs.sites.saveCalendarSites();
        }
    },
    
    siteChecked : function(event, el)
    {
        var Request = event.target;
        
        if (Request.status != 200)
        {
            // TODO: should we warn the user?
            // TODO: 3xx redirections
            return;
        }
        
        this.ObservedSites[el].ServerIsKind = (Request.getResponseHeader("Last-Modified") != ""); // TODO: works?
        
        // compute hash
        var slength = Request.responseText.length;
        var StringStream =
            Components.classes["@mozilla.org/io/string-input-stream;1"]
                .createInstance(Components.interfaces.nsIStringInputStream);
        StringStream.setData(Request.responseText, slength);
        
        var HashService = Components.classes[HASH_CONTRACT_ID].createInstance(nsICryptoHash);
        HashService.init(Components.interfaces.nsICryptoHash.MD5);
        
        HashService.updateFromStream(StringStream, slength);
        Hash = HashService.finish(true);
        
        if (this.ObservedSites[el].Hash != Hash)
        {
            // empty old hash => don't alarm
            if (this.ObservedSites[el].Hash.length) {
                this.Alarm.alarm(this.ObservedSites[el], false, false);
            }
            this.ObservedSites[el].Hash = Hash;
            // TODO: semaphore?
            this.Prefs.sites.saveObservedSites();
        }
    },
    
    setTimer : function()
    {
        if (this.Timer)
        {
            this.Timer.cancel();
        }
        else
        {
            this.Timer = Components.classes[TIMER_CONTRACT_ID].createInstance(nsITimer);
        }
        this.Timer.initWithCallback(this, this.timerInterval, nsITimer.TYPE_REPEATING_SLACK);
    },
    
    // nsITimer
    notify : function(aTimer)
    {
        this.checkSites();
    },
    
    // nsISupports
    QueryInterface : function(aIID)
    {
        if (   !aIID.equals(nsINotify)
            && !aIID.equals(nsISupports)
            && !aIID.equals(nsIObserver)
            && !aIID.equals(nsITimer)
           )
            throw Components.results.NS_ERROR_NO_INTERFACE;
        return this;
    },
    
    // nsIObserver
    observe: function(aSubject, aTopic, aData)
    {
        switch(aTopic)
        {
            case "xpcom-startup":
                var obsSvc = Components.classes[OBSERVER_CONTRACT_ID].getService(nsIObserverService);
                obsSvc.addObserver(this, "profile-after-change", true);
                break;
            
            case "profile-after-change":
                this.init();
                break;
        }
    } 
    
}; // CNotify.prototype



// #################################################################
//                             Component code
// #################################################################
// (modified from:
//    http://forums.mozillazine.org/viewtopic.php?t=308369
//    http://developer.mozilla.org/en/docs/How_to_Build_an_XPCOM_Component_in_Javascript
//    http://kb.mozillazine.org/Implementing_XPCOM_components_in_JavaScript
//    YMail Extension
// )

var Module = {

    myCID: CLASS_ID,
    myProgID: CONTRACT_ID,
    myFriendlyName: CLASS_NAME,
    
    registerSelf: function(compMgr, fileSpec, location, type)
    {
        compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
        
        compMgr.registerFactoryLocation(
            this.myCID, this.myFriendlyName, this.myProgID, fileSpec, location, type
            );
        
        Components.classes["@mozilla.org/categorymanager;1"]
            .getService(Components.interfaces.nsICategoryManager)
            .addCategoryEntry(
                "app-startup", 
                this.myFriendlyName, 
                "service," + this.myProgID, 
                true, true
            );
    },
    
    unregisterSelf: function(compMgr, location, loader)
    {
        Components.classes["@mozilla.org/categorymanager;1"]
            .getService(Components.interfaces.nsICategoryManager)
            .deleteCategoryEntry(
                "app-startup", 
                this.myFriendlyName, 
                true
            );
    },
    
    getClassObject: function(compMgr, cid, iid)
    {
        if(!cid.equals(this.myCID))
            throw Components.results.NS_ERROR_NO_INTERFACE;
        
        if(!iid.equals(Components.interfaces.nsIFactory))
            throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        
        return this.myFactory;
    },
    
    myFactory: {
        createInstance: function(outer, iid)
        {
            if (outer != null)
                throw Components.results.NS_ERROR_NO_AGGREGATION;
            
            return (new CNotify()).QueryInterface(iid);
        }
    },
    
    canUnload: function(compMgr)
    {
        return true;
    }
    
};

function NSGetModule(compMgr, fileSpec)
{
    return Module;
}
© 2006-2007 Zespół Gemme - Programy na zamówienie