512 lines
15 KiB
JavaScript
512 lines
15 KiB
JavaScript
/**
|
|
* Manages collecting user actions and events, logging them to file, and
|
|
* uploading the files to the central server.
|
|
*
|
|
* You should never need to create an instance of this prototype; access the
|
|
* CEIP (Custom Experience Improvement Program) information through |client
|
|
* .ceip|. Most code should only need to use the |logEvent| method.
|
|
*/
|
|
function CEIP()
|
|
{
|
|
// 'types' is a comma-separated sorted list.
|
|
this.enabled = false;
|
|
this.types = "";
|
|
this.commandNest = 0;
|
|
this.uploadBlock = new Object();
|
|
|
|
// Give the user a unique, but random, ID.
|
|
if (!client.prefs["ceip.userid"])
|
|
client.prefs["ceip.userid"] = randomString(32);
|
|
|
|
this.update();
|
|
setTimeout(function(self) { self.uploadLogs() }, 1000, this);
|
|
}
|
|
|
|
CEIP.prototype.destroy =
|
|
function ceip_destroy()
|
|
{
|
|
if (this.enabled)
|
|
this.stopLog();
|
|
}
|
|
|
|
CEIP.prototype.notifyUpdate =
|
|
function ceip_notifyupdate()
|
|
{
|
|
setTimeout(function(self) { self.update() }, 100, this);
|
|
}
|
|
|
|
CEIP.prototype.update =
|
|
function ceip_update()
|
|
{
|
|
var self = this;
|
|
function updateAndLogTypes(types)
|
|
{
|
|
if (self.enabled && (self.types != types.join(",")))
|
|
{
|
|
self.types = types.join(",");
|
|
self.logEvent({type: "logger", event: "types", list: self.types});
|
|
}
|
|
};
|
|
|
|
// Find out which CEIP logging types are enabled.
|
|
var types = new Array();
|
|
var prefs = client.prefManager.listPrefs("ceip.log.");
|
|
for (var i = 0; i < prefs.length; i++)
|
|
{
|
|
if ((typeof client.prefs[prefs[i]] == "boolean") && client.prefs[prefs[i]])
|
|
types.push(prefs[i].substr(9));
|
|
}
|
|
var enabled = (types.sort().length > 0);
|
|
|
|
// If we're logging currently, and the types have changed, log that change.
|
|
updateAndLogTypes(types);
|
|
|
|
// Start or stop the logging if the overall enabled state has changed.
|
|
if (!this.enabled && enabled)
|
|
this.startLog();
|
|
else if (this.enabled && !enabled)
|
|
this.stopLog();
|
|
|
|
// If we're now logging, and the types have changed, log that change.
|
|
// Note that this is done twice to ensure we always have the types logged.
|
|
updateAndLogTypes(types);
|
|
}
|
|
|
|
CEIP.prototype.getLogFolder =
|
|
function ceip_getlogfolder()
|
|
{
|
|
var logPath = new nsLocalFile(client.prefs["profilePath"]);
|
|
logPath.append("ceip");
|
|
return logPath;
|
|
}
|
|
|
|
CEIP.prototype.getLogFile =
|
|
function ceip_getlogfile()
|
|
{
|
|
var logFilePath = this.getLogFolder();
|
|
logFilePath.append("current.log");
|
|
return logFilePath;
|
|
}
|
|
|
|
CEIP.prototype.startLog =
|
|
function ceip_startlog()
|
|
{
|
|
dd("CEIP: LOGGING START");
|
|
var version = getVersionInfo();
|
|
this.enabled = true;
|
|
this.logEvent({
|
|
type: "logger",
|
|
event: "start",
|
|
userid: client.prefs["ceip.userid"],
|
|
clientVersion: __cz_version,
|
|
clientVersionSuffix: __cz_suffix,
|
|
clientLocale: this.getSelectedLocale("chatzilla"),
|
|
hostName: version.hostName,
|
|
hostVersion: version.hostVersion,
|
|
hostBuildID: version.hostBuildID,
|
|
hostLocale: this.getSelectedLocale("global"),
|
|
tz: this.getWinterTimezoneOffset(),
|
|
tzNow: (new Date()).getTimezoneOffset()
|
|
});
|
|
|
|
var self = this;
|
|
var hfn1 = function(e) { return self.hookCommandBefore(e); };
|
|
var hfn2 = function(e) { return self.hookCommandAfter(e); };
|
|
var hfn3 = function(event, cx, popup) { return self.hookMenuOpen(event, cx, popup); };
|
|
var hfn4 = function(event, cx, popup) { return self.hookMenuClose(event, cx, popup); };
|
|
|
|
// Set up all of our hooks.
|
|
if (!("beforeHooks" in client.commandManager)) {
|
|
client.commandManager.beforeHooks = new Object();
|
|
}
|
|
if (!("afterHooks" in client.commandManager)) {
|
|
client.commandManager.afterHooks = new Object();
|
|
}
|
|
client.commandManager.beforeHooks["ceip"] = hfn1;
|
|
client.commandManager.afterHooks["ceip"] = hfn2;
|
|
this.commandNest = 0;
|
|
if (client.menuManager.onCallbackPopupShowing)
|
|
throw "Someone beat CEIP to the MenuManager callback!";
|
|
client.menuManager.onCallbackPopupShowing = hfn3;
|
|
client.menuManager.onCallbackPopupHiding = hfn4;
|
|
}
|
|
|
|
CEIP.prototype.stopLog =
|
|
function ceip_stoplog()
|
|
{
|
|
// Remove all of our hooks.
|
|
if ("beforeHooks" in client.commandManager) {
|
|
delete client.commandManager.beforeHooks["ceip"];
|
|
}
|
|
if ("afterHooks" in client.commandManager) {
|
|
delete client.commandManager.afterHooks["ceip"];
|
|
}
|
|
client.menuManager.onCallbackPopupShowing = null;
|
|
client.menuManager.onCallbackPopupHiding = null;
|
|
|
|
this.logEvent({type: "logger", event: "stop"});
|
|
this.enabled = false;
|
|
dd("CEIP: LOGGING STOP");
|
|
}
|
|
|
|
/**
|
|
* Logs a single event to the log file, if CEIP is enabled.
|
|
*
|
|
* All code performing actions directly caused by the user should log CEIP
|
|
* events using |logEvent|.
|
|
*
|
|
* @param data An |Object| containing properties, including "type", to be
|
|
* logged by CEIP. All property names matching /^[-_a-z0-9]+$/i
|
|
* will be included. The time of the event is logged automatically,
|
|
* overriding any property called "time".
|
|
*/
|
|
CEIP.prototype.logEvent =
|
|
function ceip_logevent(data)
|
|
{
|
|
const NORMAL_FILE_TYPE = Components.interfaces.nsIFile.NORMAL_FILE_TYPE;
|
|
|
|
// Don't log anything we're not meant to. We always log 'logger' events.
|
|
if ((data.type != "logger") && !client.prefs["ceip.log." + data.type])
|
|
return;
|
|
|
|
var logFile = this.getLogFile();
|
|
if (!logFile.exists()) {
|
|
logFile.create(NORMAL_FILE_TYPE, 0600);
|
|
this.writeLogLine("<events>");
|
|
}
|
|
|
|
var line = " <event";
|
|
data.time = Number(new Date());
|
|
for (var prop in data) {
|
|
if (prop.match(/[^-_a-z0-9]/i)) {
|
|
// Skip anything that's not a valid name for our log.
|
|
continue;
|
|
}
|
|
line += " " + prop + '="';
|
|
line += encodeForXMLAttribute(String(data[prop])) + '"';
|
|
}
|
|
line += "/>";
|
|
this.writeLogLine(line);
|
|
|
|
// We want to return if we're:
|
|
// a) currently switching logs.
|
|
// b) haven't filled up the current log file.
|
|
if (("changeOver" in this) || (logFile.fileSize < client.prefs["ceip.uploadSize"])) {
|
|
return;
|
|
}
|
|
|
|
this.changeOver = true;
|
|
this.stopLog();
|
|
this.writeLogLine("</events>");
|
|
|
|
try
|
|
{
|
|
// Move current log to a unique filename before resuming logging.
|
|
var oldFile = this.getLogFile();
|
|
var newFile = this.getLogFile();
|
|
newFile.leafName = Number(new Date()) + ".xml";
|
|
newFile.createUnique(NORMAL_FILE_TYPE, 0600);
|
|
oldFile.moveTo(null, newFile.leafName);
|
|
|
|
/* startLog() will reset the nest, but we might be in the middle of a
|
|
* command execution right now and would rather not let that happen.
|
|
*/
|
|
var commandNest = this.commandNest;
|
|
this.startLog();
|
|
this.commandNest = commandNest;
|
|
}
|
|
catch (ex)
|
|
{
|
|
this.logEvent({type: "logger", event: "error", method: "logEvent",
|
|
error: formatException(ex)});
|
|
this.logEvent({type: "logger", event: "start"});
|
|
}
|
|
delete this.changeOver;
|
|
|
|
setTimeout(function(self) { self.uploadLogs() }, 1000, this);
|
|
}
|
|
|
|
CEIP.prototype.writeLogLine =
|
|
function ceip_writelogline(line)
|
|
{
|
|
dd("CEIP: " + line);
|
|
var logFile = new LocalFile(this.getLogFile(), ">>");
|
|
logFile.write(line + client.lineEnd);
|
|
try
|
|
{
|
|
logFile.close();
|
|
}
|
|
catch (ex)
|
|
{}
|
|
}
|
|
|
|
CEIP.prototype.uploadLogs =
|
|
function ceip_uploadlogs()
|
|
{
|
|
const nsILocalFile = Components.interfaces.nsILocalFile;
|
|
|
|
try
|
|
{
|
|
var logPath = this.getLogFolder();
|
|
var enumerator = logPath.directoryEntries;
|
|
var re = new RegExp("^\\d+\\.xml", "i");
|
|
|
|
while (enumerator.hasMoreElements())
|
|
{
|
|
var file = enumerator.getNext().QueryInterface(nsILocalFile);
|
|
if (re.test(file.leafName))
|
|
this.uploadLog(file);
|
|
}
|
|
}
|
|
catch(ex)
|
|
{
|
|
this.logEvent({type: "logger", event: "error", method: "uploadLogs",
|
|
error: formatException(ex)});
|
|
}
|
|
}
|
|
|
|
CEIP.prototype.uploadLog =
|
|
function ceip_uploadlog(file)
|
|
{
|
|
var self = this;
|
|
|
|
function uploadLogDone()
|
|
{
|
|
var rpl = request.responseText;
|
|
var ary;
|
|
|
|
if (!/^RESULT OK$/m.test(rpl))
|
|
{
|
|
ary = rpl.match(/^RESULT FAILED: (.*)$/m);
|
|
var err = ary && ary[1] ? ary[1] : request.status + " " +
|
|
request.statusText;
|
|
self.logEvent({type: "logger", event: "upload", result: "failed",
|
|
error: err});
|
|
addStatusMessage(getMsg(MSG_CEIP_UPLOAD_FAILED, [file.leafName,
|
|
err]));
|
|
return;
|
|
}
|
|
|
|
ary = rpl.match(/^ID ([\w.]+)$/m);
|
|
self.logEvent({type: "logger", event: "upload", result: "ok",
|
|
key: ary[1]});
|
|
addStatusMessage(getMsg(MSG_CEIP_UPLOAD_OK, file.leafName));
|
|
|
|
try
|
|
{
|
|
file.remove(false);
|
|
}
|
|
catch(ex)
|
|
{
|
|
self.logEvent({type: "logger", event: "error",
|
|
method: "uploadLogDone",
|
|
error: formatException(ex)});
|
|
}
|
|
};
|
|
|
|
function uploadLogError()
|
|
{
|
|
var err = request.status + " " + request.statusText;
|
|
self.logEvent({type: "logger", event: "upload", result: "failed",
|
|
error: err});
|
|
addStatusMessage(getMsg(MSG_CEIP_UPLOAD_FAILED, [file.leafName, err]));
|
|
};
|
|
|
|
try
|
|
{
|
|
// Make sure each file is only uploaded once per session.
|
|
if (file.leafName in this.uploadBlock)
|
|
return;
|
|
this.uploadBlock[file.leafName] = true;
|
|
|
|
// Create URL for sending data.
|
|
var uploadUrl = client.prefs["ceip.uploadUrl"];
|
|
uploadUrl += "?uid=" + client.prefs["ceip.userid"];
|
|
uploadUrl += "&fid=" + file.leafName;
|
|
|
|
// Read in file in chunks (FIXME: why?) into string for uploading.
|
|
var data = "";
|
|
var chunkSize = 10240;
|
|
var readLog = new LocalFile(file, "<");
|
|
while (readLog.inputStream.available() > chunkSize)
|
|
data += readLog.read(chunkSize);
|
|
data += readLog.read(readLog.inputStream.available());
|
|
readLog.close();
|
|
|
|
// Upload file using a simple POST request to the configured URL.
|
|
var request = new XMLHttpRequest();
|
|
request.onload = uploadLogDone;
|
|
request.onerror = uploadLogError;
|
|
request.open("POST", uploadUrl);
|
|
request.setRequestHeader("Content-Type", "application/xml");
|
|
request.send(data);
|
|
}
|
|
catch(ex)
|
|
{
|
|
var err = formatException(ex);
|
|
this.logEvent({type: "logger", event: "error", method: "uploadLog",
|
|
error: err});
|
|
addStatusMessage(getMsg(MSG_CEIP_UPLOAD_FAILED, [file.leafName, err]));
|
|
}
|
|
}
|
|
|
|
CEIP.prototype.getCurrentViewType =
|
|
function ceip_getcurrentviewtype()
|
|
{
|
|
try
|
|
{
|
|
if (client.currentObject && ("unicodeName" in client.currentObject))
|
|
return client.currentObject.TYPE;
|
|
}
|
|
catch (ex) {}
|
|
return MSG_UNKNOWN;
|
|
}
|
|
|
|
CEIP.prototype.getEventViewType =
|
|
function ceip_geteventviewtype(e)
|
|
{
|
|
try
|
|
{
|
|
if (("sourceObject" in e) && e.sourceObject)
|
|
return e.sourceObject.TYPE;
|
|
}
|
|
catch (ex) {}
|
|
return MSG_UNKNOWN;
|
|
}
|
|
|
|
/**
|
|
* Gets the currently selected locale for a given package.
|
|
*
|
|
* @param packageName A package to get the selected locale of, e.g. "global",
|
|
* "chatzilla".
|
|
* @returns An IETF language tag (e.g. "en-US") for the locale.
|
|
*/
|
|
CEIP.prototype.getSelectedLocale =
|
|
function ceip_getselectedlocale(packageName)
|
|
{
|
|
var selectedLocale = "";
|
|
try
|
|
{
|
|
var chromeReg = getService("@mozilla.org/chrome/chrome-registry;1",
|
|
"nsIXULChromeRegistry");
|
|
selectedLocale = chromeReg.getSelectedLocale(packageName);
|
|
}
|
|
catch (ex) {}
|
|
return selectedLocale;
|
|
}
|
|
|
|
/**
|
|
* Gets a date-independent timezone offset for the user.
|
|
*
|
|
* The calculation takes the two solstices, using approximate dates 21st June
|
|
* and 21st December, and chooses the most "behind" timezone offset of them as
|
|
* the winter timezone offset.
|
|
*
|
|
* @returns A value which is constant throughout the year for a given timezone.
|
|
*/
|
|
CEIP.prototype.getWinterTimezoneOffset =
|
|
function ceip_getwintertimezoneoffset()
|
|
{
|
|
var d1 = new Date();
|
|
d1.setMonth(5);
|
|
d1.setDate(21);
|
|
var d2 = new Date();
|
|
d2.setMonth(11);
|
|
d2.setDate(21);
|
|
return Math.max(d1.getTimezoneOffset(), d2.getTimezoneOffset());
|
|
}
|
|
|
|
CEIP.prototype.isCommandIgnored =
|
|
function ceip_iscommandignored(e)
|
|
{
|
|
return (e.command.name == "create-tab-for-view") ||
|
|
(e.command.name == "set-current-view") ||
|
|
(e.command.name == "focus-input") ||
|
|
(e.command.name == "say") ||
|
|
(e.command.name == "me") ||
|
|
(e.command.name == "msg") ||
|
|
(e.command.name == "describe");
|
|
}
|
|
|
|
CEIP.prototype.hookCommandBefore =
|
|
function ceip_hookcommandbefore(e)
|
|
{
|
|
try
|
|
{
|
|
if (this.isCommandIgnored(e))
|
|
return;
|
|
|
|
this.commandNest++;
|
|
|
|
var data = {type: "command", viewType: this.getEventViewType(e),
|
|
command: e.command.name};
|
|
|
|
if (typeof e.command.func == "string")
|
|
data.alias = true;
|
|
if (("source" in e) && (typeof e.source == "string"))
|
|
data.interactive = e.source;
|
|
else if (e.isInteractive)
|
|
data.interactive = true;
|
|
if (this.commandNest > 1)
|
|
data.nest = (this.commandNest - 1);
|
|
|
|
this.logEvent(data);
|
|
}
|
|
catch (ex)
|
|
{
|
|
this.logEvent({type: "logger", event: "error",
|
|
method: "hookCommandBefore",
|
|
error: formatException(ex)});
|
|
}
|
|
}
|
|
|
|
CEIP.prototype.hookCommandAfter =
|
|
function ceip_hookcommandafter(e)
|
|
{
|
|
try
|
|
{
|
|
if (this.isCommandIgnored(e))
|
|
return;
|
|
|
|
this.commandNest--;
|
|
}
|
|
catch (ex)
|
|
{
|
|
this.logEvent({type: "logger", event: "error",
|
|
method: "hookCommandAfter",
|
|
error: formatException(ex)});
|
|
}
|
|
}
|
|
|
|
CEIP.prototype.hookMenuOpen =
|
|
function ceip_hookmenuopen(event, cx, popup)
|
|
{
|
|
try
|
|
{
|
|
this.logEvent({type: "menu", viewType: this.getCurrentViewType(),
|
|
event: "open", name: popup.getAttribute("menuName")});
|
|
}
|
|
catch(ex)
|
|
{
|
|
this.logEvent({type: "logger", event: "error",
|
|
method: "hookMenuOpen",
|
|
error: formatException(ex)});
|
|
}
|
|
}
|
|
|
|
CEIP.prototype.hookMenuClose =
|
|
function ceip_hookmenuclose(event, cx, popup)
|
|
{
|
|
try
|
|
{
|
|
this.logEvent({type: "menu", viewType: this.getCurrentViewType(),
|
|
event: "close", name: popup.getAttribute("menuName")});
|
|
}
|
|
catch(ex)
|
|
{
|
|
this.logEvent({type: "logger", event: "error",
|
|
method: "hookMenuClose",
|
|
error: formatException(ex)});
|
|
}
|
|
}
|