0.91/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;
}