gijskruitbosch%gmail.com b6d06f2b64 Bug 503615 - force sending of CEIP data with application/xml mimetype. r=silver@warwickcompsoc.co.uk (James Ross), a=NPOTFFB.
git-svn-id: svn://10.0.0.236/trunk@258565 18797224-902f-48f8-a5cc-f745e15eee43
2009-10-03 20:52:28 +00:00

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)});
}
}