/** * 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(""); } var line = " "); 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)}); } }