Auto-merge with m-c b51803f3fdef
|
@ -45,3 +45,17 @@ include $(DEPTH)/config/autoconf.mk
|
||||||
DIRS = {972ce4c6-7e08-4474-a285-3208198ce6fd}
|
DIRS = {972ce4c6-7e08-4474-a285-3208198ce6fd}
|
||||||
|
|
||||||
include $(topsrcdir)/config/rules.mk
|
include $(topsrcdir)/config/rules.mk
|
||||||
|
|
||||||
|
ifeq (beta,$(MOZ_UPDATE_CHANNEL))
|
||||||
|
EXTENSIONS = \
|
||||||
|
testpilot@labs.mozilla.com \
|
||||||
|
$(NULL)
|
||||||
|
|
||||||
|
define _INSTALL_EXTENSIONS
|
||||||
|
$(PYTHON) $(topsrcdir)/config/nsinstall.py $(wildcard $(srcdir)/$(dir)/*) $(DIST)/bin/extensions/$(dir)
|
||||||
|
|
||||||
|
endef # do not remove the blank line!
|
||||||
|
|
||||||
|
libs::
|
||||||
|
$(foreach dir,$(EXTENSIONS),$(_INSTALL_EXTENSIONS))
|
||||||
|
endif
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
resource testpilot ./
|
||||||
|
content testpilot content/
|
||||||
|
skin testpilot skin skin/all/
|
||||||
|
skin testpilot-os skin skin/linux/ os=Linux
|
||||||
|
skin testpilot-os skin skin/mac/ os=Darwin
|
||||||
|
skin testpilot-os skin skin/win/ os=WINNT
|
||||||
|
overlay chrome://browser/content/browser.xul chrome://testpilot/content/tp-browser.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} appversion<=3.6.*
|
||||||
|
overlay chrome://browser/content/browser.xul chrome://testpilot/content/feedback-browser.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} appversion>3.7a1pre
|
||||||
|
# For the menubar on Mac
|
||||||
|
overlay chrome://testpilot/content/all-studies-window.xul chrome://browser/content/macBrowserOverlay.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} os=Darwin
|
|
@ -0,0 +1,84 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Weave.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2008
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Dan Mills <thunder@mozilla.com>
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
const Cc = Components.classes;
|
||||||
|
const Ci = Components.interfaces;
|
||||||
|
const Cu = Components.utils;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
|
function TestPilotComponent() {}
|
||||||
|
TestPilotComponent.prototype = {
|
||||||
|
classDescription: "Test Pilot Component",
|
||||||
|
contractID: "@mozilla.org/testpilot/service;1",
|
||||||
|
classID: Components.ID("{e6e5e58f-7977-485a-b076-2f74bee2677b}"),
|
||||||
|
_xpcom_categories: [{ category: "app-startup", service: true }],
|
||||||
|
_startupTimer: null,
|
||||||
|
|
||||||
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
|
||||||
|
Ci.nsISupportsWeakReference]),
|
||||||
|
|
||||||
|
observe: function TPC__observe(subject, topic, data) {
|
||||||
|
let os = Cc["@mozilla.org/observer-service;1"].
|
||||||
|
getService(Ci.nsIObserverService);
|
||||||
|
switch (topic) {
|
||||||
|
case "app-startup":
|
||||||
|
os.addObserver(this, "sessionstore-windows-restored", true);
|
||||||
|
break;
|
||||||
|
case "sessionstore-windows-restored":
|
||||||
|
/* Stop oberver, to ensure that globalStartup doesn't get
|
||||||
|
* called more than once. */
|
||||||
|
os.removeObserver(this, "sessionstore-windows-restored", false);
|
||||||
|
/* Call global startup on a timer so that it's off of the main
|
||||||
|
* thread... delay a few seconds to give firefox time to finish
|
||||||
|
* starting up.
|
||||||
|
*/
|
||||||
|
this._startupTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||||
|
this._startupTimer.initWithCallback(
|
||||||
|
{notify: function(timer) {
|
||||||
|
Cu.import("resource://testpilot/modules/setup.js");
|
||||||
|
TestPilotSetup.globalStartup();
|
||||||
|
}}, 10000, Ci.nsITimer.TYPE_ONE_SHOT);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function NSGetModule(compMgr, fileSpec) {
|
||||||
|
return XPCOMUtils.generateModule([TestPilotComponent]);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,446 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Test Pilot.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
* Raymond Lee <raymond@appcoast.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
// TODO Show individual status page in new chromeless window as html with
|
||||||
|
// background color set to "moz-dialog".
|
||||||
|
|
||||||
|
const NO_STUDIES_IMG = "chrome://testpilot/skin/testPilot_200x200.png";
|
||||||
|
const PROPOSE_STUDY_URL =
|
||||||
|
"https://wiki.mozilla.org/Labs/Test_Pilot#For_researchers";
|
||||||
|
|
||||||
|
var TestPilotXulWindow = {
|
||||||
|
_stringBundle : null,
|
||||||
|
|
||||||
|
onSubmitButton: function(experimentId) {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
let task = TestPilotSetup.getTaskById(experimentId);
|
||||||
|
let button = document.getElementById("submit-button-" + task.id);
|
||||||
|
|
||||||
|
// Hide the upload button so it doesn't get clicked again...
|
||||||
|
let parent = button.parentNode;
|
||||||
|
while (parent.firstChild) {
|
||||||
|
parent.removeChild(parent.firstChild);
|
||||||
|
}
|
||||||
|
// Replace it with a message:
|
||||||
|
this.addLabel(
|
||||||
|
parent,
|
||||||
|
this._stringBundle.getString("testpilot.studiesWindow.uploading"));
|
||||||
|
let self = this;
|
||||||
|
|
||||||
|
task.upload( function(success) {
|
||||||
|
while (parent.firstChild) {
|
||||||
|
parent.removeChild(parent.firstChild);
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
self.addThanksMessage(parent);
|
||||||
|
// TODO or should we move it to 'finished studies' immediately?
|
||||||
|
} else {
|
||||||
|
// TODO a better error message?
|
||||||
|
self.addLabel(
|
||||||
|
parent,
|
||||||
|
self._stringBundle.getString(
|
||||||
|
"testpilot.studiesWindow.unableToReachServer"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
addThanksMessage: function(container) {
|
||||||
|
// Fill in status box with icon and message to show success
|
||||||
|
let hbox = document.createElement("hbox");
|
||||||
|
container.appendChild(this.makeSpacer());
|
||||||
|
container.appendChild(hbox);
|
||||||
|
this.addLabel(
|
||||||
|
container,
|
||||||
|
this._stringBundle.getString(
|
||||||
|
"testpilot.studiesWindow.thanksForContributing"));
|
||||||
|
container.appendChild(this.makeSpacer());
|
||||||
|
hbox.appendChild(this.makeSpacer());
|
||||||
|
this.addImg(hbox, "study-submitted");
|
||||||
|
hbox.appendChild(this.makeSpacer());
|
||||||
|
},
|
||||||
|
|
||||||
|
addXulLink: function (container, text, url, openInTab) {
|
||||||
|
let linkContainer = document.createElement("hbox");
|
||||||
|
let link = document.createElement("label");
|
||||||
|
let spacer = document.createElement("spacer");
|
||||||
|
link.setAttribute("value", text);
|
||||||
|
link.setAttribute("class", "text-link");
|
||||||
|
if (openInTab) {
|
||||||
|
link.setAttribute(
|
||||||
|
"onclick",
|
||||||
|
"if (event.button==0) { " +
|
||||||
|
"TestPilotWindowUtils.openInTab('" + url + "'); }");
|
||||||
|
} else {
|
||||||
|
link.setAttribute(
|
||||||
|
"onclick",
|
||||||
|
"if (event.button==0) { " +
|
||||||
|
"TestPilotWindowUtils.openChromeless('" + url + "'); }");
|
||||||
|
}
|
||||||
|
linkContainer.appendChild(link);
|
||||||
|
spacer.setAttribute("flex", "1");
|
||||||
|
linkContainer.appendChild(spacer);
|
||||||
|
container.appendChild(linkContainer);
|
||||||
|
},
|
||||||
|
|
||||||
|
addLabel: function(container, text, styleClass) {
|
||||||
|
let label = document.createElement("label");
|
||||||
|
label.setAttribute("value", text);
|
||||||
|
if (styleClass) {
|
||||||
|
label.setAttribute("class", styleClass);
|
||||||
|
}
|
||||||
|
container.appendChild(label);
|
||||||
|
},
|
||||||
|
|
||||||
|
addImg: function(container, iconClass) {
|
||||||
|
let newImg = document.createElement("image");
|
||||||
|
newImg.setAttribute("class", iconClass);
|
||||||
|
container.appendChild(newImg);
|
||||||
|
},
|
||||||
|
|
||||||
|
makeSpacer: function() {
|
||||||
|
let spacer = document.createElement("spacer");
|
||||||
|
spacer.setAttribute("flex", "1");
|
||||||
|
return spacer;
|
||||||
|
},
|
||||||
|
|
||||||
|
addThumbnail: function(container, imgUrl) {
|
||||||
|
let boundingBox = document.createElement("vbox");
|
||||||
|
boundingBox.setAttribute("class", "results-thumbnail");
|
||||||
|
let bBox2 = document.createElement("hbox");
|
||||||
|
|
||||||
|
boundingBox.appendChild(this.makeSpacer());
|
||||||
|
boundingBox.appendChild(bBox2);
|
||||||
|
boundingBox.appendChild(this.makeSpacer());
|
||||||
|
|
||||||
|
bBox2.appendChild(this.makeSpacer());
|
||||||
|
let newImg = document.createElement("image");
|
||||||
|
newImg.setAttribute("src", imgUrl);
|
||||||
|
newImg.setAttribute("class", "results-thumbnail");
|
||||||
|
bBox2.appendChild(newImg);
|
||||||
|
bBox2.appendChild(this.makeSpacer());
|
||||||
|
|
||||||
|
container.appendChild(boundingBox);
|
||||||
|
},
|
||||||
|
|
||||||
|
addProgressBar: function(container, percent) {
|
||||||
|
let progBar = document.createElement("progressmeter");
|
||||||
|
progBar.setAttribute("mode", "determined");
|
||||||
|
progBar.setAttribute("value", Math.ceil(percent).toString());
|
||||||
|
container.appendChild(progBar);
|
||||||
|
},
|
||||||
|
|
||||||
|
addDescription: function(container, title, paragraph) {
|
||||||
|
let desc = document.createElement("description");
|
||||||
|
desc.setAttribute("class", "study-title");
|
||||||
|
let txtNode = document.createTextNode(title);
|
||||||
|
desc.appendChild(txtNode);
|
||||||
|
container.appendChild(desc);
|
||||||
|
|
||||||
|
desc = document.createElement("description");
|
||||||
|
desc.setAttribute("class", "study-description");
|
||||||
|
desc.setAttribute("crop", "none");
|
||||||
|
txtNode = document.createTextNode(paragraph);
|
||||||
|
desc.appendChild(txtNode);
|
||||||
|
container.appendChild(desc);
|
||||||
|
},
|
||||||
|
|
||||||
|
addButton: function(container, label, id, onClickHandler) {
|
||||||
|
let button = document.createElement("button");
|
||||||
|
button.setAttribute("label", label);
|
||||||
|
button.setAttribute("id", id);
|
||||||
|
button.setAttribute("oncommand", onClickHandler);
|
||||||
|
container.appendChild(button);
|
||||||
|
},
|
||||||
|
|
||||||
|
_sortNewestFirst: function(experiments) {
|
||||||
|
experiments.sort(
|
||||||
|
function sortFunc(a, b) {
|
||||||
|
if (a.endDate && b.endDate) {
|
||||||
|
return b.endDate - a.endDate;
|
||||||
|
}
|
||||||
|
if (a.publishDate && b.publishDate) {
|
||||||
|
if (isNaN(a.publishDate) || isNaN(b.publishDate)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return b.publishDate - a.publishDate;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return experiments;
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad: function () {
|
||||||
|
Components.utils.import("resource://testpilot/modules/Observers.js");
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
Components.utils.import("resource://testpilot/modules/tasks.js");
|
||||||
|
|
||||||
|
this._stringBundle = document.getElementById("testpilot-stringbundle");
|
||||||
|
this._init(false);
|
||||||
|
Observers.add("testpilot:task:changed", this._onTaskStatusChanged, this);
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnload: function() {
|
||||||
|
Observers.remove("testpilot:task:changed", this._onTaskStatusChanged, this);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onTaskStatusChanged : function() {
|
||||||
|
this._init(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
onTakeSurveyButton: function(taskId) {
|
||||||
|
let task = TestPilotSetup.getTaskById(taskId);
|
||||||
|
TestPilotWindowUtils.openChromeless(task.defaultUrl);
|
||||||
|
task.onDetailPageOpened();
|
||||||
|
},
|
||||||
|
|
||||||
|
_init: function(aReload) {
|
||||||
|
let experiments;
|
||||||
|
let ready = false;
|
||||||
|
|
||||||
|
// Are we done loading tasks?
|
||||||
|
if (TestPilotSetup.startupComplete) {
|
||||||
|
experiments = TestPilotSetup.getAllTasks();
|
||||||
|
if (experiments.length > 0 ) {
|
||||||
|
ready = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
// If you opened the window before tasks are done loading, exit now but try
|
||||||
|
// again in a few seconds.
|
||||||
|
window.setTimeout(
|
||||||
|
function() { TestPilotXulWindow._init(aReload); }, 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let numFinishedStudies = 0;
|
||||||
|
let numCurrentStudies = 0;
|
||||||
|
|
||||||
|
/* Remove 'loading' message */
|
||||||
|
let msg = window.document.getElementById("still-loading-msg");
|
||||||
|
msg.setAttribute("hidden", "true");
|
||||||
|
|
||||||
|
if (aReload) {
|
||||||
|
/* If we're reloading, start by clearing out any old stuff already
|
||||||
|
* present in the listboxes. */
|
||||||
|
let listboxIds =
|
||||||
|
["current-studies-listbox", "finished-studies-listbox",
|
||||||
|
"study-results-listbox"];
|
||||||
|
for (let i = 0; i < listboxIds.length; i++) {
|
||||||
|
let listbox = document.getElementById(listboxIds[i]);
|
||||||
|
|
||||||
|
while (listbox.lastChild) {
|
||||||
|
listbox.removeChild(listbox.lastChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
experiments = this._sortNewestFirst(experiments);
|
||||||
|
|
||||||
|
for (let i = 0; i < experiments.length; i++) {
|
||||||
|
let task = experiments[i];
|
||||||
|
let newRow = document.createElement("richlistitem");
|
||||||
|
newRow.setAttribute("class", "tp-study-list");
|
||||||
|
|
||||||
|
this.addThumbnail(newRow, task.thumbnail);
|
||||||
|
|
||||||
|
let textVbox = document.createElement("vbox");
|
||||||
|
newRow.appendChild(textVbox);
|
||||||
|
|
||||||
|
let openInTab = (task.taskType == TaskConstants.TYPE_LEGACY);
|
||||||
|
|
||||||
|
this.addDescription(textVbox, task.title, task.summary);
|
||||||
|
if (task.showMoreInfoLink) {
|
||||||
|
this.addXulLink(
|
||||||
|
textVbox, this._stringBundle.getString("testpilot.moreInfo"),
|
||||||
|
task.defaultUrl, openInTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the rightmost status area, depending on status:
|
||||||
|
let statusVbox = document.createElement("vbox");
|
||||||
|
if (task.status == TaskConstants.STATUS_FINISHED) {
|
||||||
|
this.addLabel(
|
||||||
|
statusVbox,
|
||||||
|
this._stringBundle.getFormattedString(
|
||||||
|
"testpilot.studiesWindow.finishedOn",
|
||||||
|
[(new Date(task.endDate)).toLocaleDateString()]));
|
||||||
|
this.addButton(statusVbox,
|
||||||
|
this._stringBundle.getString("testpilot.submit"),
|
||||||
|
"submit-button-" + task.id,
|
||||||
|
"TestPilotXulWindow.onSubmitButton(" + task.id + ");");
|
||||||
|
}
|
||||||
|
if (task.status == TaskConstants.STATUS_CANCELLED) {
|
||||||
|
let hbox = document.createElement("hbox");
|
||||||
|
newRow.setAttribute("class", "tp-opted-out");
|
||||||
|
statusVbox.appendChild(this.makeSpacer());
|
||||||
|
statusVbox.appendChild(hbox);
|
||||||
|
this.addLabel(
|
||||||
|
statusVbox,
|
||||||
|
this._stringBundle.getString("testpilot.studiesWindow.canceledStudy"));
|
||||||
|
statusVbox.appendChild(this.makeSpacer());
|
||||||
|
hbox.appendChild(this.makeSpacer());
|
||||||
|
this.addImg(hbox, "study-canceled");
|
||||||
|
hbox.appendChild(this.makeSpacer());
|
||||||
|
}
|
||||||
|
if (task.status == TaskConstants.STATUS_NEW ||
|
||||||
|
task.status == TaskConstants.STATUS_PENDING ) {
|
||||||
|
newRow.setAttribute("class", "tp-new-results");
|
||||||
|
|
||||||
|
if (task.taskType == TaskConstants.TYPE_SURVEY) {
|
||||||
|
this.addButton(
|
||||||
|
statusVbox,
|
||||||
|
this._stringBundle.getString("testpilot.takeSurvey"),
|
||||||
|
"survey-button",
|
||||||
|
"TestPilotXulWindow.onTakeSurveyButton('" + task.id + "');");
|
||||||
|
} else if (task.taskType == TaskConstants.TYPE_EXPERIMENT) {
|
||||||
|
if (task.startDate) {
|
||||||
|
this.addLabel(
|
||||||
|
statusVbox,
|
||||||
|
this._stringBundle.getFormattedString(
|
||||||
|
"testpilot.studiesWindow.willStart",
|
||||||
|
[(new Date(task.startDate)).toLocaleDateString()]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (task.status == TaskConstants.STATUS_IN_PROGRESS ||
|
||||||
|
task.status == TaskConstants.STATUS_STARTING) {
|
||||||
|
|
||||||
|
if (task.taskType == TaskConstants.TYPE_SURVEY) {
|
||||||
|
this.addButton(
|
||||||
|
statusVbox,
|
||||||
|
this._stringBundle.getString("testpilot.takeSurvey"),
|
||||||
|
"survey-button",
|
||||||
|
"TestPilotXulWindow.onTakeSurveyButton('" + task.id + "');");
|
||||||
|
} else if (task.taskType == TaskConstants.TYPE_EXPERIMENT) {
|
||||||
|
this.addLabel(
|
||||||
|
statusVbox,
|
||||||
|
this._stringBundle.getString(
|
||||||
|
"testpilot.studiesWindow.gatheringData"));
|
||||||
|
let now = (new Date()).getTime();
|
||||||
|
let progress =
|
||||||
|
100 * (now - task.startDate) / (task.endDate - task.startDate);
|
||||||
|
this.addProgressBar(statusVbox, progress);
|
||||||
|
this.addLabel(
|
||||||
|
statusVbox,
|
||||||
|
this._stringBundle.getFormattedString(
|
||||||
|
"testpilot.studiesWindow.willFinish",
|
||||||
|
[(new Date(task.endDate)).toLocaleDateString()]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (task.status >= TaskConstants.STATUS_SUBMITTED) {
|
||||||
|
if (task.taskType == TaskConstants.TYPE_RESULTS) {
|
||||||
|
let maintask = TestPilotSetup.getTaskById(task.relatedStudyId);
|
||||||
|
if (maintask && maintask.status >= TaskConstants.STATUS_SUBMITTED) {
|
||||||
|
this.addThanksMessage(statusVbox);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (task.status == TaskConstants.STATUS_MISSED) {
|
||||||
|
// TODO use Sean's icon for missed studies
|
||||||
|
} else {
|
||||||
|
this.addThanksMessage(statusVbox);
|
||||||
|
numFinishedStudies ++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let spacer = document.createElement("spacer");
|
||||||
|
spacer.setAttribute("flex", "1");
|
||||||
|
newRow.appendChild(spacer);
|
||||||
|
newRow.appendChild(statusVbox);
|
||||||
|
|
||||||
|
// Use status to decide which panel to add this to:
|
||||||
|
let rowset;
|
||||||
|
if (task.taskType == TaskConstants.TYPE_RESULTS) {
|
||||||
|
rowset = document.getElementById("study-results-listbox");
|
||||||
|
} else if (task.status > TaskConstants.STATUS_FINISHED) {
|
||||||
|
rowset = document.getElementById("finished-studies-listbox");
|
||||||
|
} else {
|
||||||
|
rowset = document.getElementById("current-studies-listbox");
|
||||||
|
numCurrentStudies++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO further distinguish by background colors.
|
||||||
|
rowset.appendChild(newRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no current studies, show a message about upcoming
|
||||||
|
// studies:
|
||||||
|
if (numCurrentStudies == 0) {
|
||||||
|
let newRow = document.createElement("richlistitem");
|
||||||
|
newRow.setAttribute("class", "tp-study-list");
|
||||||
|
this.addThumbnail(newRow, NO_STUDIES_IMG);
|
||||||
|
let textVbox = document.createElement("vbox");
|
||||||
|
textVbox.setAttribute("class", "pilot-largetext");
|
||||||
|
newRow.appendChild(textVbox);
|
||||||
|
this.addDescription(
|
||||||
|
textVbox, "",
|
||||||
|
this._stringBundle.getString("testpilot.studiesWindow.noStudies"));
|
||||||
|
this.addXulLink(
|
||||||
|
textVbox,
|
||||||
|
this._stringBundle.getString("testpilot.studiesWindow.proposeStudy"),
|
||||||
|
PROPOSE_STUDY_URL, true);
|
||||||
|
document.getElementById("current-studies-listbox").appendChild(newRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show number of studies the user finished on badge:
|
||||||
|
document.getElementById("num-finished-badge").setAttribute(
|
||||||
|
"value", numFinishedStudies);
|
||||||
|
|
||||||
|
window.sizeToContent();
|
||||||
|
},
|
||||||
|
|
||||||
|
focusPane: function(paneIndex) {
|
||||||
|
document.getElementById("tp-xulwindow-deck").selectedIndex = paneIndex;
|
||||||
|
|
||||||
|
// When you focus the 'study findings' tab, any results there which
|
||||||
|
// are still marked "new" should have their status changed as the user
|
||||||
|
// is considered to have seen them.
|
||||||
|
if (paneIndex == 2) {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
Components.utils.import("resource://testpilot/modules/tasks.js");
|
||||||
|
|
||||||
|
let experiments = TestPilotSetup.getAllTasks();
|
||||||
|
for each (let experiment in experiments) {
|
||||||
|
if (experiment.taskType == TaskConstants.TYPE_RESULTS) {
|
||||||
|
if (experiment.status == TaskConstants.STATUS_NEW) {
|
||||||
|
experiment.changeStatus(TaskConstants.STATUS_ARCHIVED, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
|
||||||
|
<?xml-stylesheet href="chrome://global/skin/preferences.css" type="text/css"?>
|
||||||
|
<?xml-stylesheet href="chrome://mozapps/content/extensions/extensions.css"
|
||||||
|
type="text/css"?>
|
||||||
|
<?xml-stylesheet href="chrome://testpilot/content/browser.css" type="text/css"?>
|
||||||
|
|
||||||
|
|
||||||
|
<!DOCTYPE prefwindow [
|
||||||
|
<!ENTITY % testpilotDTD SYSTEM "chrome://testpilot/locale/main.dtd">
|
||||||
|
%testpilotDTD;
|
||||||
|
]>
|
||||||
|
|
||||||
|
<prefwindow id="test-pilot-all-studies-window"
|
||||||
|
title="&testpilot.studiesWindow.title;"
|
||||||
|
windowtype="extensions:testpilot:all_studies_window"
|
||||||
|
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||||
|
onload="TestPilotXulWindow.onLoad();"
|
||||||
|
onunload="TestPilotXulWindow.onUnload();">
|
||||||
|
|
||||||
|
<script src="chrome://testpilot/content/window-utils.js"
|
||||||
|
type="application/javascript;version=1.8"/>
|
||||||
|
<script src="chrome://testpilot/content/all-studies-window.js"
|
||||||
|
type="application/javascript;version=1.8"/>
|
||||||
|
|
||||||
|
<stringbundleset id="stringbundleset">
|
||||||
|
<stringbundle id="testpilot-stringbundle"
|
||||||
|
src="chrome://testpilot/locale/main.properties" />
|
||||||
|
</stringbundleset>
|
||||||
|
|
||||||
|
<vbox flex="1">
|
||||||
|
<windowdragbox orient="vertical">
|
||||||
|
<radiogroup id="tp-radiogroup" orient="horizontal" role="listbox"
|
||||||
|
class="paneSelector"
|
||||||
|
onselect="TestPilotXulWindow.focusPane(this.selectedIndex);">
|
||||||
|
<radio pane="current-studies-pane-button" orient="vertical">
|
||||||
|
<image class="paneButtonIcon" />
|
||||||
|
<label class="paneButtonLabel"
|
||||||
|
value="&testpilot.studiesWindow.currentStudies.label;"/>
|
||||||
|
</radio>
|
||||||
|
<radio pane="finished-studies-pane-button" orient="vertical">
|
||||||
|
<stack align="center" pack="center">
|
||||||
|
<hbox align="center" pack="center">
|
||||||
|
<image class="paneButtonIcon" />
|
||||||
|
</hbox>
|
||||||
|
<label id="num-finished-badge" class="pane-button-badge"/>
|
||||||
|
</stack>
|
||||||
|
<label class="paneButtonLabel"
|
||||||
|
value="&testpilot.studiesWindow.finishedStudies.label;"/>
|
||||||
|
</radio>
|
||||||
|
<radio pane="study-results-pane-button" orient="vertical">
|
||||||
|
<image class="paneButtonIcon" />
|
||||||
|
<label class="paneButtonLabel"
|
||||||
|
value="&testpilot.studiesWindow.studyFindings.label;"/>
|
||||||
|
</radio>
|
||||||
|
<radio pane="settings-pane-button" orient="vertical">
|
||||||
|
<image class="paneButtonIcon" />
|
||||||
|
<label class="paneButtonLabel"
|
||||||
|
value="&testpilot.studiesWindow.settings.label;"/>
|
||||||
|
</radio>
|
||||||
|
</radiogroup>
|
||||||
|
</windowdragbox>
|
||||||
|
|
||||||
|
<deck id="tp-xulwindow-deck" flex="1">
|
||||||
|
<prefpane id="current-studies-pane" class="tp-tab-panel">
|
||||||
|
<richlistbox id="current-studies-listbox" class="tp-study-list"
|
||||||
|
disabled="true">
|
||||||
|
<richlistitem id="still-loading-msg" class="tp-study-list">
|
||||||
|
<description class="pilot-largetext">
|
||||||
|
&testpilot.studiesWindow.stillLoadingMessage;
|
||||||
|
</description>
|
||||||
|
</richlistitem>
|
||||||
|
</richlistbox>
|
||||||
|
</prefpane>
|
||||||
|
|
||||||
|
<prefpane id="finished-studies-pane" class="tp-tab-panel">
|
||||||
|
<richlistbox id="finished-studies-listbox" class="tp-study-list"
|
||||||
|
disabled="true" />
|
||||||
|
</prefpane>
|
||||||
|
|
||||||
|
<prefpane id="study-results-pane" class="tp-tab-panel">
|
||||||
|
<richlistbox id="study-results-listbox" class="tp-study-list"
|
||||||
|
disabled="true" />
|
||||||
|
</prefpane>
|
||||||
|
|
||||||
|
<prefpane id="settings-pane" class="tp-tab-panel">
|
||||||
|
<preferences>
|
||||||
|
<preference id="notify-finished" type="bool"
|
||||||
|
name="extensions.testpilot.popup.showOnStudyFinished"/>
|
||||||
|
<preference id="notify-new" type="bool"
|
||||||
|
name="extensions.testpilot.popup.showOnNewStudy"/>
|
||||||
|
<preference id="notify-results" type="bool"
|
||||||
|
name="extensions.testpilot.popup.showOnNewResults"/>
|
||||||
|
<preference id="always-submit-data" type="bool"
|
||||||
|
name="extensions.testpilot.alwaysSubmitData"/>
|
||||||
|
</preferences>
|
||||||
|
<vbox style="padding: 12px;">
|
||||||
|
<groupbox>
|
||||||
|
<caption label="Data Submission" />
|
||||||
|
<checkbox label="&testpilot.settings.alwaysSubmitData.label;"
|
||||||
|
preference="always-submit-data"/>
|
||||||
|
</groupbox>
|
||||||
|
<groupbox>
|
||||||
|
<caption label="Notifications" />
|
||||||
|
<label value="&testpilot.settings.notifyMeWhen.label;"/>
|
||||||
|
<hbox>
|
||||||
|
<separator orient="vertical" />
|
||||||
|
<vbox>
|
||||||
|
<checkbox label="&testpilot.settings.readyToSubmit.label;"
|
||||||
|
preference="notify-finished"/>
|
||||||
|
<checkbox label="&testpilot.settings.newStudy.label;"
|
||||||
|
preference="notify-new"/>
|
||||||
|
<checkbox label="&testpilot.settings.hasNewResults.label;"
|
||||||
|
preference="notify-results"/>
|
||||||
|
</vbox>
|
||||||
|
</hbox>
|
||||||
|
</groupbox>
|
||||||
|
</vbox>
|
||||||
|
</prefpane>
|
||||||
|
</deck>
|
||||||
|
</vbox>
|
||||||
|
|
||||||
|
<!-- For the menubar on mac, the below elements are needed for
|
||||||
|
macBrowserOverlay.xul to overlay this window. -->
|
||||||
|
<stringbundleset id="stringbundleset"/>
|
||||||
|
|
||||||
|
<commandset id="mainCommandSet"/>
|
||||||
|
<commandset id="baseMenuCommandSet"/>
|
||||||
|
<commandset id="placesCommands"/>
|
||||||
|
|
||||||
|
<broadcasterset id="mainBroadcasterSet"/>
|
||||||
|
|
||||||
|
<keyset id="mainKeyset"/>
|
||||||
|
<keyset id="baseMenuKeyset"/>
|
||||||
|
|
||||||
|
<menubar id="main-menubar" style="border:none !important;margin:0;padding:0;"/>
|
||||||
|
|
||||||
|
</prefwindow>
|
|
@ -0,0 +1,240 @@
|
||||||
|
/* Toolbar Button */
|
||||||
|
|
||||||
|
#feedback-menu-button {
|
||||||
|
-moz-box-orient: horizontal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#feedback-menu-button .toolbarbutton-text {
|
||||||
|
display: -moz-box;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#feedback-menu-button .toolbarbutton-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#feedback-menu-button .toolbarbutton-menu-dropmarker {
|
||||||
|
-moz-padding-start: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pilot-notifications-button {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Popup Bounding Box */
|
||||||
|
#pilot-notification-popup {
|
||||||
|
-moz-appearance: none;
|
||||||
|
-moz-window-shadow: none;
|
||||||
|
background-color: transparent;
|
||||||
|
margin-top: -6px;
|
||||||
|
margin-right: -3px;
|
||||||
|
width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tail-up {
|
||||||
|
-moz-border-image: url(chrome://testpilot-os/skin/notification-tail-up.png) 26 56 22 18 / 26px 56px 22px 18px round stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tail-down {
|
||||||
|
-moz-border-image: url(chrome://testpilot-os/skin/notification-tail-down.png) 26 56 22 18 / 26px 56px 22px 18px round stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.pilot-notification-popup-container {
|
||||||
|
-moz-appearance: none;
|
||||||
|
margin-right: -42px;
|
||||||
|
padding: 0px 5px 5px 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pilot-notification-toprow {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pilot-notification-text,
|
||||||
|
#pilot-notification-link {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pilot-notification-close {
|
||||||
|
list-style-image: url("chrome://testpilot-os/skin/close_button.png");
|
||||||
|
-moz-image-region: rect(0px, 14px, 14px, 0px);
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pilot-notification-close:hover {
|
||||||
|
-moz-image-region: rect(0px, 28px, 14px, 14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pilot-notification-close:hover:active {
|
||||||
|
-moz-image-region: rect(0px, 42px, 14px, 28px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pilot-notify-me-when[disabled="true"] {
|
||||||
|
color: -moz-dialogtext;
|
||||||
|
}
|
||||||
|
.pilot-title {
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
image.study-finished {
|
||||||
|
list-style-image: url("chrome://testpilot/skin/tp-submit-48x48.png");
|
||||||
|
height: 48px;
|
||||||
|
width: 48px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
image.study-submitted {
|
||||||
|
list-style-image: url("chrome://testpilot/skin/status-completed.png");
|
||||||
|
height: 32px;
|
||||||
|
width: 64px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
image.study-canceled {
|
||||||
|
list-style-image: url("chrome://testpilot/skin/status-ejected.png");
|
||||||
|
height: 32px;
|
||||||
|
width: 64px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
image.new-study {
|
||||||
|
list-style-image: url("chrome://testpilot/skin/tp-study-48x48.png");
|
||||||
|
height: 48px;
|
||||||
|
width: 48px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
image.new-results {
|
||||||
|
list-style-image: url("chrome://testpilot/skin/tp-results-48x48.png");
|
||||||
|
height: 48px;
|
||||||
|
width: 48px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
image.update-extension {
|
||||||
|
list-style-image: url("chrome://testpilot/skin/testpilot_32x32.png");
|
||||||
|
height: 48px;
|
||||||
|
width: 48px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
image.study-result {
|
||||||
|
list-style-image: url("chrome://testpilot/skin/badge-default.png");
|
||||||
|
height: 96px;
|
||||||
|
width: 96px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* All studies window */
|
||||||
|
.pilot-largetext {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#test-pilot-all-studies-window {
|
||||||
|
min-width: 720px !important;
|
||||||
|
min-height: 500px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#test-pilot-all-studies-window > .prefWindow-dlgbuttons {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paneSelector {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paneSelector radio[pane="current-studies-pane-button"] .paneButtonIcon {
|
||||||
|
list-style-image: url("chrome://testpilot/skin/tp-currentstudies-32x32.png");
|
||||||
|
padding-top: 3px;
|
||||||
|
}
|
||||||
|
.paneSelector radio[pane="finished-studies-pane-button"] .paneButtonIcon {
|
||||||
|
list-style-image: url("chrome://testpilot/skin/tp-completedstudies-32x32.png");
|
||||||
|
padding-top: 3px;
|
||||||
|
}
|
||||||
|
.paneSelector radio[pane="study-results-pane-button"] .paneButtonIcon {
|
||||||
|
list-style-image: url("chrome://testpilot/skin/tp-learned-32x32.png");
|
||||||
|
padding-top: 3px;
|
||||||
|
}
|
||||||
|
.paneSelector radio[pane="settings-pane-button"] .paneButtonIcon {
|
||||||
|
list-style-image: url("chrome://testpilot/skin/tp-settings-32x32.png");
|
||||||
|
padding-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane-button-badge {
|
||||||
|
background-color: green;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 2px;
|
||||||
|
-moz-border-radius: 100%;
|
||||||
|
margin-right: 25px;
|
||||||
|
margin-bottom: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
richlistbox.tp-study-list {
|
||||||
|
height: 522px;
|
||||||
|
width: 502px;
|
||||||
|
overflow: auto;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-tab-panel {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
description.study-description {
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
description.study-title {
|
||||||
|
width: 350px;
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: left;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
richlistitem.tp-study-list {
|
||||||
|
min-height: 120px;
|
||||||
|
color: black;
|
||||||
|
background-color: -moz-dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
richlistitem.tp-new-results {
|
||||||
|
min-height: 120px;
|
||||||
|
color: black;
|
||||||
|
background-color: LemonChiffon;
|
||||||
|
}
|
||||||
|
|
||||||
|
richlistitem.tp-opted-out {
|
||||||
|
min-height: 120px;
|
||||||
|
color: grey;
|
||||||
|
background-color: -moz-dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
vbox.results-thumbnail {
|
||||||
|
height: 120px;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
image.results-thumbnail {
|
||||||
|
max-height: 90px;
|
||||||
|
max-width: 90px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-link {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefpane .groupbox-body {
|
||||||
|
-moz-appearance: none;
|
||||||
|
padding: 8px 4px 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefpane .groupbox-title {
|
||||||
|
background: url("chrome://global/skin/50pct_transparent_grey.png") repeat-x bottom left;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Test Pilot.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Atul Varma <atul@mozilla.com>
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
var TestPilotMenuUtils;
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
var Cc = Components.classes;
|
||||||
|
var Cu = Components.utils;
|
||||||
|
var Ci = Components.interfaces;
|
||||||
|
|
||||||
|
Cu.import("resource://testpilot/modules/setup.js");
|
||||||
|
|
||||||
|
TestPilotMenuUtils = {
|
||||||
|
updateSubmenu: function() {
|
||||||
|
let ntfyMenuFinished =
|
||||||
|
document.getElementById("pilot-menu-notify-finished");
|
||||||
|
let ntfyMenuNew = document.getElementById("pilot-menu-notify-new");
|
||||||
|
let ntfyMenuResults = document.getElementById("pilot-menu-notify-results");
|
||||||
|
let alwaysSubmitData =
|
||||||
|
document.getElementById("pilot-menu-always-submit-data");
|
||||||
|
let Application = Cc["@mozilla.org/fuel/application;1"]
|
||||||
|
.getService(Ci.fuelIApplication);
|
||||||
|
ntfyMenuFinished.setAttribute("checked", Application.prefs.getValue(
|
||||||
|
POPUP_SHOW_ON_FINISH, false));
|
||||||
|
ntfyMenuNew.setAttribute("checked", Application.prefs.getValue(
|
||||||
|
POPUP_SHOW_ON_NEW, false));
|
||||||
|
ntfyMenuResults.setAttribute("checked", Application.prefs.getValue(
|
||||||
|
POPUP_SHOW_ON_RESULTS, false));
|
||||||
|
alwaysSubmitData.setAttribute("checked", Application.prefs.getValue(
|
||||||
|
ALWAYS_SUBMIT_DATA, false));
|
||||||
|
},
|
||||||
|
|
||||||
|
togglePref: function(id) {
|
||||||
|
let prefName = "extensions.testpilot." + id;
|
||||||
|
let oldVal = Application.prefs.getValue(prefName, false);
|
||||||
|
Application.prefs.setValue( prefName, !oldVal);
|
||||||
|
|
||||||
|
// If you turn on or off the global pref, startup or shutdown test pilot
|
||||||
|
// accordingly:
|
||||||
|
if (prefName == RUN_AT_ALL_PREF) {
|
||||||
|
if (oldVal == true) {
|
||||||
|
TestPilotSetup.globalShutdown();
|
||||||
|
}
|
||||||
|
if (oldVal == false) {
|
||||||
|
TestPilotSetup.globalStartup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onPopupShowing: function(event) {
|
||||||
|
this._setMenuLabels();
|
||||||
|
},
|
||||||
|
|
||||||
|
onPopupHiding: function(event) {
|
||||||
|
let target = event.target;
|
||||||
|
if (target.id == "pilot-menu-popup") {
|
||||||
|
let menu = document.getElementById("pilot-menu");
|
||||||
|
if (target.parentNode != menu) {
|
||||||
|
menu.appendChild(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_setMenuLabels: function() {
|
||||||
|
// Make the enable/disable User Studies menu item show the right label
|
||||||
|
// for the current status...
|
||||||
|
let runStudiesToggle = document.getElementById("feedback-menu-enable-studies");
|
||||||
|
if (runStudiesToggle) {
|
||||||
|
let currSetting = Application.prefs.getValue("extensions.testpilot.runStudies",
|
||||||
|
true);
|
||||||
|
|
||||||
|
let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"].
|
||||||
|
getService(Ci.nsIStringBundleService).
|
||||||
|
createBundle("chrome://testpilot/locale/main.properties");
|
||||||
|
|
||||||
|
if (currSetting) {
|
||||||
|
runStudiesToggle.setAttribute("label",
|
||||||
|
stringBundle.GetStringFromName("testpilot.turnOff"));
|
||||||
|
} else {
|
||||||
|
runStudiesToggle.setAttribute("label",
|
||||||
|
stringBundle.GetStringFromName("testpilot.turnOn"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let studiesMenuItem = document.getElementById("feedback-menu-show-studies");
|
||||||
|
studiesMenuItem.setAttribute("disabled",
|
||||||
|
!Application.prefs.getValue(RUN_AT_ALL_PREF, true));
|
||||||
|
},
|
||||||
|
|
||||||
|
onMenuButtonMouseDown: function(attachPointId) {
|
||||||
|
if (!attachPointId) {
|
||||||
|
attachPointId = "pilot-notifications-button";
|
||||||
|
}
|
||||||
|
let menuPopup = document.getElementById("pilot-menu-popup");
|
||||||
|
let menuButton = document.getElementById(attachPointId);
|
||||||
|
|
||||||
|
if (menuPopup.parentNode != menuButton)
|
||||||
|
menuButton.appendChild(menuPopup);
|
||||||
|
|
||||||
|
let alignment;
|
||||||
|
// Menu should appear above status bar icon, but below Feedback button
|
||||||
|
if (attachPointId == "pilot-notifications-button") {
|
||||||
|
alignment = "before_start";
|
||||||
|
} else {
|
||||||
|
alignment = "after_end";
|
||||||
|
}
|
||||||
|
|
||||||
|
menuPopup.openPopup(menuButton, alignment, 0, 0, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var TestPilotWindowHandlers = {
|
||||||
|
onWindowLoad: function() {
|
||||||
|
/* "Hold" window load events for TestPilotSetup, passing them along only
|
||||||
|
* after startup is complete. It's hacky, but the benefit is that
|
||||||
|
* TestPilotSetup.onWindowLoad can treat all windows the same no matter
|
||||||
|
* whether they opened with Firefox on startup or were opened later. */
|
||||||
|
if (TestPilotSetup.startupComplete) {
|
||||||
|
TestPilotSetup.onWindowLoad(window);
|
||||||
|
} else {
|
||||||
|
let observerSvc = Cc["@mozilla.org/observer-service;1"]
|
||||||
|
.getService(Ci.nsIObserverService);
|
||||||
|
let observer = {
|
||||||
|
observe: function(subject, topic, data) {
|
||||||
|
observerSvc.removeObserver(this, "testpilot:startup:complete");
|
||||||
|
TestPilotSetup.onWindowLoad(window);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
observerSvc.addObserver(observer, "testpilot:startup:complete", false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onWindowUnload: function() {
|
||||||
|
TestPilotSetup.onWindowUnload(window);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("load", TestPilotWindowHandlers.onWindowLoad, false);
|
||||||
|
window.addEventListener("unload", TestPilotWindowHandlers.onWindowUnload, false);
|
||||||
|
}());
|
||||||
|
|
|
@ -0,0 +1,223 @@
|
||||||
|
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
|
||||||
|
<html> <head>
|
||||||
|
<title>Test Pilot Debug Page</title>
|
||||||
|
<script src="experiment-page.js" type="application/javascript;version=1.8"></script>
|
||||||
|
<script type="application/javascript;version=1.8">
|
||||||
|
|
||||||
|
function getEid() {
|
||||||
|
var selector = document.getElementById("task-selector");
|
||||||
|
var i = selector.selectedIndex;
|
||||||
|
return selector.options[i].getAttribute("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTaskStatus() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
var newStatus = document.getElementById("status-code").value;
|
||||||
|
newStatus = parseInt(newStatus);
|
||||||
|
var task = TestPilotSetup.getTaskById(getEid());
|
||||||
|
task.changeStatus(newStatus, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadAllExperiments() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
TestPilotSetup.reloadRemoteExperiments();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runUnitTests() {
|
||||||
|
Components.utils.import("resource://testpilot/tests/test_data_store.js");
|
||||||
|
runAllTests();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testJarStore() {
|
||||||
|
var Cuddlefish = {};
|
||||||
|
Components.utils.import("resource://testpilot/modules/lib/cuddlefish.js",
|
||||||
|
Cuddlefish);
|
||||||
|
var loader = new Cuddlefish.Loader(
|
||||||
|
{rootPaths: ["resource://testpilot/modules/",
|
||||||
|
"resource://testpilot/modules/lib/"]});
|
||||||
|
var jarStoreModule = loader.require("jar-code-store");
|
||||||
|
var SecurableModule = loader.require("securable-module");
|
||||||
|
var jarStore = new jarStoreModule.JarStore();
|
||||||
|
|
||||||
|
// OK now watch this! It's gonna be awesome!
|
||||||
|
var clientLoader = Cuddlefish.Loader(
|
||||||
|
{fs: new SecurableModule.CompositeFileSystem(
|
||||||
|
[jarStore, loader.fs])});
|
||||||
|
dump("Debug page Requiring toolbar study.\n");
|
||||||
|
var toolbarStudy = clientLoader.require("toolbar-study");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function remindMe() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
TestPilotSetup._notifyUserOfTasks();
|
||||||
|
//TestPilotSetup._doHousekeeping();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCodeStorage() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
var loader = TestPilotSetup._remoteExperimentLoader;
|
||||||
|
return loader._jarStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedFilename() {
|
||||||
|
var selector = document.getElementById("file-selector");
|
||||||
|
var i = selector.selectedIndex;
|
||||||
|
return selector.options[i].text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadExperimentCode() {
|
||||||
|
var filename = getSelectedFilename();
|
||||||
|
var codeStore = getCodeStorage();
|
||||||
|
var textArea = document.getElementById("experiment-code-area");
|
||||||
|
var path = codeStore.resolveModule(null, filename);
|
||||||
|
code = codeStore.getFile(path).contents;
|
||||||
|
textArea.value = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAndRun() {
|
||||||
|
var filename = getSelectedFilename();
|
||||||
|
var codeStore = getCodeStorage();
|
||||||
|
var path = codeStore.resolveModule(null, filename);
|
||||||
|
var textArea = document.getElementById("experiment-code-area");
|
||||||
|
codeStore.setLocalOverride(path, textArea.value);
|
||||||
|
reloadAllExperiments(function(success) {
|
||||||
|
document.getElementById("debug").innerHTML = "Success? " + success;});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMetaData() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
let task = TestPilotSetup.getTaskById(getEid());
|
||||||
|
let json = task._prependMetadataToJSON(function(json) {
|
||||||
|
document.getElementById("debug").innerHTML = json;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeThereBeAPopup() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
var task = TestPilotSetup.getTaskById(getEid());
|
||||||
|
var text = "Sample popup for " + task.title;
|
||||||
|
TestPilotSetup._showNotification(task, false, text, "This is Title",
|
||||||
|
"", true, false, "Linktext",
|
||||||
|
"http://evilbrainjono.net");
|
||||||
|
}
|
||||||
|
|
||||||
|
function wipeDb() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
var task = TestPilotSetup.getTaskById(getEid());
|
||||||
|
task.dataStore.wipeAllData();
|
||||||
|
var debug = document.getElementById("debug");
|
||||||
|
debug.innerHTML = "Wiped!";
|
||||||
|
}
|
||||||
|
|
||||||
|
function nukeDb() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
var task = TestPilotSetup.getTaskById(getEid());
|
||||||
|
task.dataStore.nukeTable();
|
||||||
|
var debug = document.getElementById("debug");
|
||||||
|
debug.innerHTML = "Nuked!";
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateFileDropdown() {
|
||||||
|
var codeStore = getCodeStorage();
|
||||||
|
var files = codeStore.listAllFiles();
|
||||||
|
var selector = document.getElementById("file-selector");
|
||||||
|
var opt, i;
|
||||||
|
for (var i = 0; i < files.length; i++) {
|
||||||
|
opt = document.createElement("option");
|
||||||
|
opt.innerHTML = files[i];
|
||||||
|
selector.appendChild(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
selector = document.getElementById("task-selector");
|
||||||
|
var tasks = TestPilotSetup.getAllTasks();
|
||||||
|
var title;
|
||||||
|
for (i = 0; i < tasks.length; i++) {
|
||||||
|
opt = document.createElement("option");
|
||||||
|
title = tasks[i].title;
|
||||||
|
opt.innerHTML = title;
|
||||||
|
opt.setAttribute("value", tasks[i].id);
|
||||||
|
selector.appendChild(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSelectedTaskStatus() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
var task = TestPilotSetup.getTaskById(getEid());
|
||||||
|
document.getElementById("show-status-span").innerHTML = task.status;
|
||||||
|
var selector = document.getElementById("status-selector");
|
||||||
|
selector.selectedIndex = task.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSelectedTask() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
var task = TestPilotSetup.getTaskById(getEid());
|
||||||
|
task.changeStatus(0, true);
|
||||||
|
var prefService = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
|
||||||
|
var prefName= "extensions.testpilot.startDate." + task.id;
|
||||||
|
if (prefService.prefHasUserValue(prefName)) {
|
||||||
|
prefService.clearUserPref(prefName);
|
||||||
|
}
|
||||||
|
//TestPilotSetup.reloadRemoteExperiments();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedTaskStatus() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
var task = TestPilotSetup.getTaskById(getEid());
|
||||||
|
|
||||||
|
var selector = document.getElementById("status-selector");
|
||||||
|
var i = selector.selectedIndex;
|
||||||
|
var newStatus = parseInt( selector.options[i].value );
|
||||||
|
task.changeStatus(newStatus, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
canvas { border: 1px solid black; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body onload="populateFileDropdown();showSelectedTaskStatus();">
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<p><select id="task-selector" onchange="showSelectedTaskStatus();"></select> Current Status = <span id="show-status-span"></span>.
|
||||||
|
<button onclick="resetSelectedTask();showSelectedTaskStatus();">Reset Task</button>
|
||||||
|
or set it to
|
||||||
|
<select id="status-selector" onchange="setSelectedTaskStatus(); showSelectedTaskStatus();">
|
||||||
|
<option value="0">0 (New)</option>
|
||||||
|
<option value="1">1 (Pending)</option>
|
||||||
|
<option value="2">2 (Starting)</option>
|
||||||
|
<option value="3">3 (In Progress)</option>
|
||||||
|
<option value="4">4 (Finished)</option>
|
||||||
|
<option value="5">5 (Cancelled)</option>
|
||||||
|
<option value="6">6 (Submitted)</option>
|
||||||
|
<option value="7">7 (Results)</option>
|
||||||
|
<option value="8">8 (Archived)</option>
|
||||||
|
</select>
|
||||||
|
<button onclick="wipeDb();">Wipe My Data</button>
|
||||||
|
<button onclick="nukeDb();">NUKE</button>
|
||||||
|
<button onclick="uploadData();">Upload My Data</button>
|
||||||
|
<button onclick="showMetaData();">Show Metadata</button>
|
||||||
|
<button onclick="runUnitTests();">Run Tests</button>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<p><button onclick="makeThereBeAPopup();">Show Dummy Popup</button>
|
||||||
|
<button onclick="reloadAllExperiments();">Reload All Experiments</button>
|
||||||
|
<button onclick="remindMe();">Notify Me</button>
|
||||||
|
<button onclick="testJarStore();">Test Jar Store</button>
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
<p><span id="debug"></span></p>
|
||||||
|
|
||||||
|
|
||||||
|
<textarea id="experiment-code-area" rows="40" cols="80">
|
||||||
|
</textarea>
|
||||||
|
<select id="file-selector"></select>
|
||||||
|
<button onclick="loadExperimentCode();">Load Ye Code</button>
|
||||||
|
<button onclick="saveAndRun();">Save And Run Ye Code</button>
|
||||||
|
|
||||||
|
|
||||||
|
</body> </html>
|
|
@ -0,0 +1,457 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Test Pilot.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
* Raymond Lee <raymond@appcoast.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
const PAGE_TYPE_STATUS = 0;
|
||||||
|
const PAGE_TYPE_QUIT = 1;
|
||||||
|
var stringBundle;
|
||||||
|
|
||||||
|
function showRawData(experimentId) {
|
||||||
|
window.openDialog(
|
||||||
|
"chrome://testpilot/content/raw-data-dialog.xul",
|
||||||
|
"TestPilotRawDataDialog", "chrome,centerscreen,resizable,scrollbars",
|
||||||
|
experimentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUrlParam(name) {
|
||||||
|
// from http://www.netlobo.com/url_query_string_javascript.html
|
||||||
|
name = name.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
|
||||||
|
var regexS = "[\\?&]"+name+"=([^&#]*)";
|
||||||
|
var regex = new RegExp(regexS);
|
||||||
|
var results = regex.exec(window.location.href);
|
||||||
|
if( results == null )
|
||||||
|
return "";
|
||||||
|
else
|
||||||
|
return results[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadData() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
let eid = parseInt(getUrlParam("eid"));
|
||||||
|
let task = TestPilotSetup.getTaskById(eid);
|
||||||
|
|
||||||
|
// If always-submit-checkbox is checked, set the pref
|
||||||
|
if (task._recursAutomatically) {
|
||||||
|
let checkBox = document.getElementById("always-submit-checkbox");
|
||||||
|
if (checkBox && checkBox.checked) {
|
||||||
|
task.setRecurPref(TaskConstants.ALWAYS_SUBMIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let uploadStatus = document.getElementById("upload-status");
|
||||||
|
uploadStatus.innerHTML =
|
||||||
|
stringBundle.GetStringFromName("testpilot.statusPage.uploadingData");
|
||||||
|
task.upload( function(success) {
|
||||||
|
if (success) {
|
||||||
|
window.location =
|
||||||
|
"chrome://testpilot/content/status.html?eid=" + eid;
|
||||||
|
} else {
|
||||||
|
uploadStatus.innerHTML =
|
||||||
|
"<p>" +
|
||||||
|
stringBundle.GetStringFromName("testpilot.statusPage.uploadError") +
|
||||||
|
"</p>";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteData() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
Components.utils.import("resource://testpilot/modules/tasks.js");
|
||||||
|
let eid = parseInt(getUrlParam("eid"));
|
||||||
|
let task = TestPilotSetup.getTaskById(eid);
|
||||||
|
task.dataStore.wipeAllData();
|
||||||
|
// reload the URL after wiping all data.
|
||||||
|
window.location = "chrome://testpilot/content/status.html?eid=" + eid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCanvas(canvas) {
|
||||||
|
const nsIFilePicker = Components.interfaces.nsIFilePicker;
|
||||||
|
let filePicker = Components.classes["@mozilla.org/filepicker;1"].
|
||||||
|
createInstance(nsIFilePicker);
|
||||||
|
filePicker.init(window, null, nsIFilePicker.modeSave);
|
||||||
|
filePicker.appendFilters(
|
||||||
|
nsIFilePicker.filterImages | nsIFilePicker.filterAll);
|
||||||
|
filePicker.defaultString = "canvas.png";
|
||||||
|
|
||||||
|
let response = filePicker.show();
|
||||||
|
if (response == nsIFilePicker.returnOK ||
|
||||||
|
response == nsIFilePicker.returnReplace) {
|
||||||
|
const nsIWebBrowserPersist = Components.interfaces.nsIWebBrowserPersist;
|
||||||
|
let file = filePicker.file;
|
||||||
|
|
||||||
|
// create a data url from the canvas and then create URIs of the source
|
||||||
|
// and targets
|
||||||
|
let io = Components.classes["@mozilla.org/network/io-service;1"].
|
||||||
|
getService(Components.interfaces.nsIIOService);
|
||||||
|
let source = io.newURI(canvas.toDataURL("image/png", ""), "UTF8", null);
|
||||||
|
let target = io.newFileURI(file);
|
||||||
|
|
||||||
|
// prepare to save the canvas data
|
||||||
|
let persist = Components.classes[
|
||||||
|
"@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].
|
||||||
|
createInstance(nsIWebBrowserPersist);
|
||||||
|
persist.persistFlags = nsIWebBrowserPersist.
|
||||||
|
PERSIST_FLAGS_REPLACE_EXISTING_FILES;
|
||||||
|
persist.persistFlags |= nsIWebBrowserPersist.
|
||||||
|
PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
|
||||||
|
|
||||||
|
// displays a download dialog (remove these 3 lines for silent download)
|
||||||
|
let xfer = Components.classes["@mozilla.org/transfer;1"].
|
||||||
|
createInstance(Components.interfaces.nsITransfer);
|
||||||
|
xfer.init(source, target, "", null, null, null, persist);
|
||||||
|
persist.progressListener = xfer;
|
||||||
|
|
||||||
|
// save the canvas data to the file
|
||||||
|
persist.saveURI(source, null, null, null, null, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportData() {
|
||||||
|
const nsIFilePicker = Components.interfaces.nsIFilePicker;
|
||||||
|
let filePicker = Components.classes["@mozilla.org/filepicker;1"].
|
||||||
|
createInstance(nsIFilePicker);
|
||||||
|
let eid = parseInt(getUrlParam("eid"));
|
||||||
|
let task = TestPilotSetup.getTaskById(eid);
|
||||||
|
|
||||||
|
filePicker.init(window, null, nsIFilePicker.modeSave);
|
||||||
|
filePicker.appendFilters(
|
||||||
|
nsIFilePicker.filterImages | nsIFilePicker.filterAll);
|
||||||
|
filePicker.defaultString = task.title + ".csv";
|
||||||
|
|
||||||
|
let response = filePicker.show();
|
||||||
|
if (response == nsIFilePicker.returnOK ||
|
||||||
|
response == nsIFilePicker.returnReplace) {
|
||||||
|
const nsIWebBrowserPersist = Components.interfaces.nsIWebBrowserPersist;
|
||||||
|
let foStream =
|
||||||
|
Components.classes["@mozilla.org/network/file-output-stream;1"].
|
||||||
|
createInstance(Components.interfaces.nsIFileOutputStream);
|
||||||
|
let converter =
|
||||||
|
Components.classes["@mozilla.org/intl/converter-output-stream;1"].
|
||||||
|
createInstance(Components.interfaces.nsIConverterOutputStream);
|
||||||
|
let file = filePicker.file;
|
||||||
|
let dataStore = task.dataStore;
|
||||||
|
let columnNames = dataStore.getHumanReadableColumnNames();
|
||||||
|
let propertyNames = dataStore.getPropertyNames();
|
||||||
|
let csvString = "";
|
||||||
|
|
||||||
|
// titles
|
||||||
|
for (let i = 0; i < columnNames.length; i++) {
|
||||||
|
csvString += "\"" + columnNames[i] + "\",";
|
||||||
|
}
|
||||||
|
if (csvString.length > 0) {
|
||||||
|
csvString = csvString.substring(0, (csvString.length - 1));
|
||||||
|
csvString += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
dataStore.getAllDataAsJSON(true, function(rawData) {
|
||||||
|
// data
|
||||||
|
for (let i = 0; i < rawData.length; i++) {
|
||||||
|
for (let j = 0; j < columnNames.length; j++) {
|
||||||
|
csvString += "\"" + rawData[i][propertyNames[j]] + "\",";
|
||||||
|
}
|
||||||
|
csvString = csvString.substring(0, (csvString.length - 1));
|
||||||
|
csvString += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// write, create, truncate
|
||||||
|
foStream.init(file, 0x02 | 0x08 | 0x20, 0664, 0);
|
||||||
|
converter.init(foStream, "UTF-8", 0, 0);
|
||||||
|
converter.writeString(csvString);
|
||||||
|
converter.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLink(url) {
|
||||||
|
// open the link in the chromeless window
|
||||||
|
let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
||||||
|
.getService(Components.interfaces.nsIWindowMediator);
|
||||||
|
let recentWindow = wm.getMostRecentWindow("navigator:browser");
|
||||||
|
|
||||||
|
if (recentWindow) {
|
||||||
|
recentWindow.TestPilotWindowUtils.openInTab(url);
|
||||||
|
} else {
|
||||||
|
window.open(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTestEndingDate(experimentId) {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
var task = TestPilotSetup.getTaskById(experimentId);
|
||||||
|
var endDate = new Date(task.endDate);
|
||||||
|
var diff = (endDate - Date.now());
|
||||||
|
var span = document.getElementById("test-end-time");
|
||||||
|
if (!span) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (diff < 0) {
|
||||||
|
span.innerHTML =
|
||||||
|
stringBundle.GetStringFromName("testpilot.statusPage.endedAlready");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var hours = diff / (60 * 60 * 1000);
|
||||||
|
if (hours < 24) {
|
||||||
|
span.innerHTML =
|
||||||
|
stringBundle.formatStringFromName(
|
||||||
|
"testpilot.statusPage.todayAt", [endDate.toLocaleTimeString()], 1);
|
||||||
|
} else {
|
||||||
|
span.innerHTML =
|
||||||
|
stringBundle.formatStringFromName(
|
||||||
|
"testpilot.statusPage.endOn", [endDate.toLocaleString()], 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMetaData() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/metadata.js");
|
||||||
|
MetadataCollector.getMetadata(function(md) {
|
||||||
|
var mdLocale = document.getElementById("md-locale");
|
||||||
|
if (mdLocale)
|
||||||
|
mdLocale.innerHTML = md.location;
|
||||||
|
var mdVersion = document.getElementById("md-version");
|
||||||
|
if (mdVersion)
|
||||||
|
mdVersion.innerHTML = md.version;
|
||||||
|
var mdOs = document.getElementById("md-os");
|
||||||
|
if (mdOs)
|
||||||
|
mdOs.innerHTML = md.operatingSystem;
|
||||||
|
var mdNumExt = document.getElementById("md-num-ext");
|
||||||
|
if (mdNumExt) {
|
||||||
|
var numExt = md.extensions.length;
|
||||||
|
if (numExt == 1) {
|
||||||
|
mdNumExt.innerHTML =
|
||||||
|
stringBundle.GetStringFromName("testpilot.statusPage.extension");
|
||||||
|
} else {
|
||||||
|
mdNumExt.innerHTML =
|
||||||
|
stringBundle.formatStringFromName(
|
||||||
|
"testpilot.statusPage.extensions", [numExt], 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onQuitPageLoad() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
setStrings(PAGE_TYPE_QUIT);
|
||||||
|
let eid = parseInt(getUrlParam("eid"));
|
||||||
|
let task = TestPilotSetup.getTaskById(eid);
|
||||||
|
let header = document.getElementById("about-quit-title");
|
||||||
|
header.innerHTML =
|
||||||
|
stringBundle.formatStringFromName(
|
||||||
|
"testpilot.quitPage.aboutToQuit", [task.title], 1);
|
||||||
|
|
||||||
|
if (task._recursAutomatically) {
|
||||||
|
document.getElementById("recur-options").setAttribute("style", "");
|
||||||
|
document.getElementById("recur-checkbox-container").
|
||||||
|
setAttribute("style", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function quitExperiment() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
Components.utils.import("resource://testpilot/modules/tasks.js");
|
||||||
|
let eid = parseInt(getUrlParam("eid"));
|
||||||
|
let reason = document.getElementById("reason-for-quit").value;
|
||||||
|
let task = TestPilotSetup.getTaskById(eid);
|
||||||
|
task.optOut(reason, function(success) {
|
||||||
|
// load the you-are-canceleed page.
|
||||||
|
window.location = "chrome://testpilot/content/status.html?eid=" + eid;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If opt-out-forever checkbox is checked, opt out forever!
|
||||||
|
if (task._recursAutomatically) {
|
||||||
|
let checkBox = document.getElementById("opt-out-forever");
|
||||||
|
if (checkBox.checked) {
|
||||||
|
task.setRecurPref(TaskConstants.NEVER_SUBMIT);
|
||||||
|
}
|
||||||
|
// quit test so rescheduling
|
||||||
|
task._reschedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRecurSettings() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
let eid = parseInt(getUrlParam("eid"));
|
||||||
|
let experiment = TestPilotSetup.getTaskById(eid);
|
||||||
|
let recurSelector = document.getElementById("recur-selector");
|
||||||
|
let newValue = recurSelector.options[recurSelector.selectedIndex].value;
|
||||||
|
experiment.setRecurPref(parseInt(newValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRecurControls(experiment) {
|
||||||
|
Components.utils.import("resource://testpilot/modules/tasks.js");
|
||||||
|
let recurPrefSpan = document.getElementById("recur-pref");
|
||||||
|
if (!recurPrefSpan) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let days = experiment._recurrenceInterval;
|
||||||
|
recurPrefSpan.innerHTML =
|
||||||
|
stringBundle.formatStringFromName(
|
||||||
|
"testpilot.statusPage.recursEveryNumberOfDays", [days], 1);
|
||||||
|
|
||||||
|
let controls = document.getElementById("recur-controls");
|
||||||
|
let selector = document.createElement("select");
|
||||||
|
controls.appendChild(selector);
|
||||||
|
selector.setAttribute("onchange", "updateRecurSettings();");
|
||||||
|
selector.setAttribute("id", "recur-selector");
|
||||||
|
|
||||||
|
let option = document.createElement("option");
|
||||||
|
option.setAttribute("value", TaskConstants.ASK_EACH_TIME);
|
||||||
|
if (experiment.recurPref == TaskConstants.ASK_EACH_TIME) {
|
||||||
|
option.setAttribute("selected", "true");
|
||||||
|
}
|
||||||
|
option.innerHTML =
|
||||||
|
stringBundle.GetStringFromName(
|
||||||
|
"testpilot.statusPage.askMeBeforeSubmitData");
|
||||||
|
selector.appendChild(option);
|
||||||
|
|
||||||
|
option = document.createElement("option");
|
||||||
|
option.setAttribute("value", TaskConstants.ALWAYS_SUBMIT);
|
||||||
|
if (experiment.recurPref == TaskConstants.ALWAYS_SUBMIT) {
|
||||||
|
option.setAttribute("selected", "true");
|
||||||
|
}
|
||||||
|
option.innerHTML =
|
||||||
|
stringBundle.GetStringFromName(
|
||||||
|
"testpilot.statusPage.alwaysSubmitData");
|
||||||
|
selector.appendChild(option);
|
||||||
|
|
||||||
|
option = document.createElement("option");
|
||||||
|
option.setAttribute("value", TaskConstants.NEVER_SUBMIT);
|
||||||
|
if (experiment.recurPref == TaskConstants.NEVER_SUBMIT) {
|
||||||
|
option.setAttribute("selected", "true");
|
||||||
|
}
|
||||||
|
option.innerHTML =
|
||||||
|
stringBundle.GetStringFromName(
|
||||||
|
"testpilot.statusPage.neverSubmitData");
|
||||||
|
selector.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadExperimentPage() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
Components.utils.import("resource://testpilot/modules/tasks.js");
|
||||||
|
var contentDiv = document.getElementById("experiment-specific-text");
|
||||||
|
var dataPrivacyDiv = document.getElementById("data-privacy-text");
|
||||||
|
// Get experimentID from the GET args of page
|
||||||
|
var eid = parseInt(getUrlParam("eid"));
|
||||||
|
var experiment = TestPilotSetup.getTaskById(eid);
|
||||||
|
if (!experiment) {
|
||||||
|
// Possible that experiments aren't done loading yet. Try again in
|
||||||
|
// a few seconds.
|
||||||
|
contentDiv.innerHTML =
|
||||||
|
stringBundle.GetStringFromName("testpilot.statusPage.loading");
|
||||||
|
window.setTimeout(function() { loadExperimentPage(); }, 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
experiment.getWebContent(function(webContent) {
|
||||||
|
contentDiv.innerHTML = webContent;
|
||||||
|
});
|
||||||
|
|
||||||
|
experiment.getDataPrivacyContent(function(dataPrivacyContent) {
|
||||||
|
if (dataPrivacyContent && dataPrivacyContent.length > 0) {
|
||||||
|
dataPrivacyDiv.innerHTML = dataPrivacyContent;
|
||||||
|
dataPrivacyDiv.removeAttribute("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Metadata and start/end date should be filled in for every experiment:
|
||||||
|
showMetaData();
|
||||||
|
getTestEndingDate(eid);
|
||||||
|
if (experiment._recursAutomatically &&
|
||||||
|
experiment.status != TaskConstants.STATUS_FINISHED) {
|
||||||
|
showRecurControls(experiment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do whatever the experiment's web content wants done on load:
|
||||||
|
experiment.webContent.onPageLoad(experiment, document, jQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStatusPageLoad() {
|
||||||
|
setStrings(PAGE_TYPE_STATUS);
|
||||||
|
/* If an experiment ID (eid) is provided in the url params, show status
|
||||||
|
* for that experiment. If not, show the main menu with status for all
|
||||||
|
* installed experiments. */
|
||||||
|
let eidString = getUrlParam("eid");
|
||||||
|
if (eidString == "") {
|
||||||
|
showStatusMenuPage();
|
||||||
|
} else {
|
||||||
|
loadExperimentPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStrings(pageType) {
|
||||||
|
stringBundle =
|
||||||
|
Components.classes["@mozilla.org/intl/stringbundle;1"].
|
||||||
|
getService(Components.interfaces.nsIStringBundleService).
|
||||||
|
createBundle("chrome://testpilot/locale/main.properties");
|
||||||
|
let map;
|
||||||
|
let mapLength;
|
||||||
|
|
||||||
|
if (pageType == PAGE_TYPE_STATUS) {
|
||||||
|
map = [
|
||||||
|
{ id: "page-title", stringKey: "testpilot.fullBrandName" },
|
||||||
|
{ id: "comments-and-discussions-link",
|
||||||
|
stringKey: "testpilot.page.commentsAndDiscussions" },
|
||||||
|
{ id: "propose-test-link",
|
||||||
|
stringKey: "testpilot.page.proposeATest" },
|
||||||
|
{ id: "testpilot-twitter-link",
|
||||||
|
stringKey: "testpilot.page.testpilotOnTwitter" }
|
||||||
|
];
|
||||||
|
} else if (pageType == PAGE_TYPE_QUIT) {
|
||||||
|
map = [
|
||||||
|
{ id: "page-title", stringKey: "testpilot.fullBrandName" },
|
||||||
|
{ id: "comments-and-discussions-link",
|
||||||
|
stringKey: "testpilot.page.commentsAndDiscussions" },
|
||||||
|
{ id: "propose-test-link",
|
||||||
|
stringKey: "testpilot.page.proposeATest" },
|
||||||
|
{ id: "testpilot-twitter-link",
|
||||||
|
stringKey: "testpilot.page.testpilotOnTwitter" },
|
||||||
|
{ id: "optional-message",
|
||||||
|
stringKey: "testpilot.quitPage.optionalMessage" },
|
||||||
|
{ id: "reason-text",
|
||||||
|
stringKey: "testpilot.quitPage.reason" },
|
||||||
|
{ id: "recur-options",
|
||||||
|
stringKey: "testpilot.quitPage.recurringStudy" },
|
||||||
|
{ id: "quit-forever-text",
|
||||||
|
stringKey: "testpilot.quitPage.quitFoever" },
|
||||||
|
{ id: "quit-study-link",
|
||||||
|
stringKey: "testpilot.quitPage.quitStudy" }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
mapLength = map.length;
|
||||||
|
for (let i = 0; i < mapLength; i++) {
|
||||||
|
let entry = map[i];
|
||||||
|
document.getElementById(entry.id).innerHTML =
|
||||||
|
stringBundle.GetStringFromName(entry.stringKey);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<?xml-stylesheet href="chrome://testpilot/content/browser.css" type="text/css"?>
|
||||||
|
<?xml-stylesheet href="chrome://testpilot-os/skin/feedback.css" type="text/css"?>
|
||||||
|
|
||||||
|
<!DOCTYPE overlay [
|
||||||
|
<!ENTITY % testpilotDTD SYSTEM "chrome://testpilot/locale/main.dtd">
|
||||||
|
%testpilotDTD;
|
||||||
|
]>
|
||||||
|
|
||||||
|
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||||
|
<script src="chrome://testpilot/content/browser.js"
|
||||||
|
type="application/x-javascript" />
|
||||||
|
<script src="chrome://testpilot/content/window-utils.js"
|
||||||
|
type="application/x-javascript" />
|
||||||
|
|
||||||
|
<menupopup id="menu_ToolsPopup">
|
||||||
|
<menu id="pilot-menu" insertafter="menu_openAddons" />
|
||||||
|
</menupopup>
|
||||||
|
|
||||||
|
<toolbarpalette id="BrowserToolbarPalette">
|
||||||
|
<toolbarbutton type="menu" id="feedback-menu-button"
|
||||||
|
class="toolbarbutton-1" label="&testpilot.feedbackbutton.label;"
|
||||||
|
onmousedown="event.preventDefault();
|
||||||
|
TestPilotMenuUtils.onMenuButtonMouseDown('feedback-menu-button');"/>
|
||||||
|
</toolbarpalette>
|
||||||
|
|
||||||
|
<toolbar id="nav-bar">
|
||||||
|
<panel id="pilot-notification-popup" hidden="true" noautofocus="true"
|
||||||
|
level="parent" position="after_start">
|
||||||
|
<vbox class="pilot-notification-popup-container">
|
||||||
|
<hbox class="pilot-notification-toprow">
|
||||||
|
<image id="pilot-notification-icon" />
|
||||||
|
<vbox pack="center">
|
||||||
|
<label id="pilot-notification-title" class="pilot-title" />
|
||||||
|
</vbox>
|
||||||
|
<spacer flex="1" />
|
||||||
|
<vbox pack="start">
|
||||||
|
<image id="pilot-notification-close"
|
||||||
|
tooltiptext="&testpilot.notification.close.tooltip;" />
|
||||||
|
</vbox>
|
||||||
|
</hbox>
|
||||||
|
<description id="pilot-notification-text" />
|
||||||
|
<hbox align="right"><label id="pilot-notification-link" /></hbox>
|
||||||
|
<hbox>
|
||||||
|
<checkbox id="pilot-notification-always-submit-checkbox"
|
||||||
|
label="&testpilot.settings.alwaysSubmitData.label;" />
|
||||||
|
<spacer flex="1" />
|
||||||
|
</hbox>
|
||||||
|
<hbox align="right">
|
||||||
|
<button id="pilot-notification-submit" />
|
||||||
|
</hbox>
|
||||||
|
</vbox>
|
||||||
|
</panel>
|
||||||
|
</toolbar>
|
||||||
|
|
||||||
|
<menu id="pilot-menu" class="menu-iconic"
|
||||||
|
label="&testpilot.feedbackbutton.label;"
|
||||||
|
insertafter="addonsManager">
|
||||||
|
<menupopup id="pilot-menu-popup"
|
||||||
|
onpopupshowing="TestPilotMenuUtils.onPopupShowing(event);"
|
||||||
|
onpopuphiding="TestPilotMenuUtils.onPopupHiding(event);">
|
||||||
|
<menuitem id="feedback-menu-happy-button"
|
||||||
|
class="menuitem-iconic"
|
||||||
|
image="chrome://testpilot-os/skin/feedback-smile-16x16.png"
|
||||||
|
label = "&testpilot.happy.label;"
|
||||||
|
oncommand="TestPilotWindowUtils.openFeedbackPage(true);"/>
|
||||||
|
<menuitem id="feedback-menu-sad-button"
|
||||||
|
class="menuitem-iconic"
|
||||||
|
image="chrome://testpilot-os/skin/feedback-frown-16x16.png"
|
||||||
|
label = "&testpilot.sad.label;"
|
||||||
|
oncommand="TestPilotWindowUtils.openFeedbackPage(false);"/>
|
||||||
|
<menuseparator/>
|
||||||
|
<menuitem id="feedback-menu-show-studies"
|
||||||
|
label="&testpilot.allStudies.label;..."
|
||||||
|
oncommand="TestPilotWindowUtils.openAllStudiesWindow();"/>
|
||||||
|
<menuitem id="feedback-menu-enable-studies"
|
||||||
|
label="&testpilot.enable.label;"
|
||||||
|
oncommand="TestPilotMenuUtils.togglePref('runStudies');"/>
|
||||||
|
</menupopup>
|
||||||
|
</menu>
|
||||||
|
</overlay>
|
|
@ -0,0 +1,174 @@
|
||||||
|
/* Plugin for jQuery for working with colors.
|
||||||
|
*
|
||||||
|
* Version 1.0.
|
||||||
|
*
|
||||||
|
* Inspiration from jQuery color animation plugin by John Resig.
|
||||||
|
*
|
||||||
|
* Released under the MIT license by Ole Laursen, October 2009.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
|
||||||
|
* var c = $.color.extract($("#mydiv"), 'background-color');
|
||||||
|
* console.log(c.r, c.g, c.b, c.a);
|
||||||
|
* $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
|
||||||
|
*
|
||||||
|
* Note that .scale() and .add() work in-place instead of returning
|
||||||
|
* new objects.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
jQuery.color = {};
|
||||||
|
|
||||||
|
// construct color object with some convenient chainable helpers
|
||||||
|
jQuery.color.make = function (r, g, b, a) {
|
||||||
|
var o = {};
|
||||||
|
o.r = r || 0;
|
||||||
|
o.g = g || 0;
|
||||||
|
o.b = b || 0;
|
||||||
|
o.a = a != null ? a : 1;
|
||||||
|
|
||||||
|
o.add = function (c, d) {
|
||||||
|
for (var i = 0; i < c.length; ++i)
|
||||||
|
o[c.charAt(i)] += d;
|
||||||
|
return o.normalize();
|
||||||
|
};
|
||||||
|
|
||||||
|
o.scale = function (c, f) {
|
||||||
|
for (var i = 0; i < c.length; ++i)
|
||||||
|
o[c.charAt(i)] *= f;
|
||||||
|
return o.normalize();
|
||||||
|
};
|
||||||
|
|
||||||
|
o.toString = function () {
|
||||||
|
if (o.a >= 1.0) {
|
||||||
|
return "rgb("+[o.r, o.g, o.b].join(",")+")";
|
||||||
|
} else {
|
||||||
|
return "rgba("+[o.r, o.g, o.b, o.a].join(",")+")";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
o.normalize = function () {
|
||||||
|
function clamp(min, value, max) {
|
||||||
|
return value < min ? min: (value > max ? max: value);
|
||||||
|
}
|
||||||
|
|
||||||
|
o.r = clamp(0, parseInt(o.r), 255);
|
||||||
|
o.g = clamp(0, parseInt(o.g), 255);
|
||||||
|
o.b = clamp(0, parseInt(o.b), 255);
|
||||||
|
o.a = clamp(0, o.a, 1);
|
||||||
|
return o;
|
||||||
|
};
|
||||||
|
|
||||||
|
o.clone = function () {
|
||||||
|
return jQuery.color.make(o.r, o.b, o.g, o.a);
|
||||||
|
};
|
||||||
|
|
||||||
|
return o.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract CSS color property from element, going up in the DOM
|
||||||
|
// if it's "transparent"
|
||||||
|
jQuery.color.extract = function (elem, css) {
|
||||||
|
var c;
|
||||||
|
do {
|
||||||
|
c = elem.css(css).toLowerCase();
|
||||||
|
// keep going until we find an element that has color, or
|
||||||
|
// we hit the body
|
||||||
|
if (c != '' && c != 'transparent')
|
||||||
|
break;
|
||||||
|
elem = elem.parent();
|
||||||
|
} while (!jQuery.nodeName(elem.get(0), "body"));
|
||||||
|
|
||||||
|
// catch Safari's way of signalling transparent
|
||||||
|
if (c == "rgba(0, 0, 0, 0)")
|
||||||
|
c = "transparent";
|
||||||
|
|
||||||
|
return jQuery.color.parse(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse CSS color string (like "rgb(10, 32, 43)" or "#fff"),
|
||||||
|
// returns color object
|
||||||
|
jQuery.color.parse = function (str) {
|
||||||
|
var res, m = jQuery.color.make;
|
||||||
|
|
||||||
|
// Look for rgb(num,num,num)
|
||||||
|
if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))
|
||||||
|
return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10));
|
||||||
|
|
||||||
|
// Look for rgba(num,num,num,num)
|
||||||
|
if (res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))
|
||||||
|
return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4]));
|
||||||
|
|
||||||
|
// Look for rgb(num%,num%,num%)
|
||||||
|
if (res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))
|
||||||
|
return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55);
|
||||||
|
|
||||||
|
// Look for rgba(num%,num%,num%,num)
|
||||||
|
if (res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))
|
||||||
|
return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55, parseFloat(res[4]));
|
||||||
|
|
||||||
|
// Look for #a0b1c2
|
||||||
|
if (res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))
|
||||||
|
return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16));
|
||||||
|
|
||||||
|
// Look for #fff
|
||||||
|
if (res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))
|
||||||
|
return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16));
|
||||||
|
|
||||||
|
// Otherwise, we're most likely dealing with a named color
|
||||||
|
var name = jQuery.trim(str).toLowerCase();
|
||||||
|
if (name == "transparent")
|
||||||
|
return m(255, 255, 255, 0);
|
||||||
|
else {
|
||||||
|
res = lookupColors[name];
|
||||||
|
return m(res[0], res[1], res[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lookupColors = {
|
||||||
|
aqua:[0,255,255],
|
||||||
|
azure:[240,255,255],
|
||||||
|
beige:[245,245,220],
|
||||||
|
black:[0,0,0],
|
||||||
|
blue:[0,0,255],
|
||||||
|
brown:[165,42,42],
|
||||||
|
cyan:[0,255,255],
|
||||||
|
darkblue:[0,0,139],
|
||||||
|
darkcyan:[0,139,139],
|
||||||
|
darkgrey:[169,169,169],
|
||||||
|
darkgreen:[0,100,0],
|
||||||
|
darkkhaki:[189,183,107],
|
||||||
|
darkmagenta:[139,0,139],
|
||||||
|
darkolivegreen:[85,107,47],
|
||||||
|
darkorange:[255,140,0],
|
||||||
|
darkorchid:[153,50,204],
|
||||||
|
darkred:[139,0,0],
|
||||||
|
darksalmon:[233,150,122],
|
||||||
|
darkviolet:[148,0,211],
|
||||||
|
fuchsia:[255,0,255],
|
||||||
|
gold:[255,215,0],
|
||||||
|
green:[0,128,0],
|
||||||
|
indigo:[75,0,130],
|
||||||
|
khaki:[240,230,140],
|
||||||
|
lightblue:[173,216,230],
|
||||||
|
lightcyan:[224,255,255],
|
||||||
|
lightgreen:[144,238,144],
|
||||||
|
lightgrey:[211,211,211],
|
||||||
|
lightpink:[255,182,193],
|
||||||
|
lightyellow:[255,255,224],
|
||||||
|
lime:[0,255,0],
|
||||||
|
magenta:[255,0,255],
|
||||||
|
maroon:[128,0,0],
|
||||||
|
navy:[0,0,128],
|
||||||
|
olive:[128,128,0],
|
||||||
|
orange:[255,165,0],
|
||||||
|
pink:[255,192,203],
|
||||||
|
purple:[128,0,128],
|
||||||
|
violet:[128,0,128],
|
||||||
|
red:[255,0,0],
|
||||||
|
silver:[192,192,192],
|
||||||
|
white:[255,255,255],
|
||||||
|
yellow:[255,255,0]
|
||||||
|
};
|
||||||
|
})();
|
1
browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.colorhelpers.min.js
поставляемый
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(function(){jQuery.color={};jQuery.color.make=function(E,D,B,C){var F={};F.r=E||0;F.g=D||0;F.b=B||0;F.a=C!=null?C:1;F.add=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]+=H}return F.normalize()};F.scale=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]*=H}return F.normalize()};F.toString=function(){if(F.a>=1){return"rgb("+[F.r,F.g,F.b].join(",")+")"}else{return"rgba("+[F.r,F.g,F.b,F.a].join(",")+")"}};F.normalize=function(){function G(I,J,H){return J<I?I:(J>H?H:J)}F.r=G(0,parseInt(F.r),255);F.g=G(0,parseInt(F.g),255);F.b=G(0,parseInt(F.b),255);F.a=G(0,F.a,1);return F};F.clone=function(){return jQuery.color.make(F.r,F.b,F.g,F.a)};return F.normalize()};jQuery.color.extract=function(C,B){var D;do{D=C.css(B).toLowerCase();if(D!=""&&D!="transparent"){break}C=C.parent()}while(!jQuery.nodeName(C.get(0),"body"));if(D=="rgba(0, 0, 0, 0)"){D="transparent"}return jQuery.color.parse(D)};jQuery.color.parse=function(E){var D,B=jQuery.color.make;if(D=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10))}if(D=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10),parseFloat(D[4]))}if(D=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55)}if(D=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55,parseFloat(D[4]))}if(D=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(E)){return B(parseInt(D[1],16),parseInt(D[2],16),parseInt(D[3],16))}if(D=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(E)){return B(parseInt(D[1]+D[1],16),parseInt(D[2]+D[2],16),parseInt(D[3]+D[3],16))}var C=jQuery.trim(E).toLowerCase();if(C=="transparent"){return B(255,255,255,0)}else{D=A[C];return B(D[0],D[1],D[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})();
|
|
@ -0,0 +1,156 @@
|
||||||
|
/*
|
||||||
|
Flot plugin for showing a crosshair, thin lines, when the mouse hovers
|
||||||
|
over the plot.
|
||||||
|
|
||||||
|
crosshair: {
|
||||||
|
mode: null or "x" or "y" or "xy"
|
||||||
|
color: color
|
||||||
|
lineWidth: number
|
||||||
|
}
|
||||||
|
|
||||||
|
Set the mode to one of "x", "y" or "xy". The "x" mode enables a
|
||||||
|
vertical crosshair that lets you trace the values on the x axis, "y"
|
||||||
|
enables a horizontal crosshair and "xy" enables them both. "color" is
|
||||||
|
the color of the crosshair (default is "rgba(170, 0, 0, 0.80)"),
|
||||||
|
"lineWidth" is the width of the drawn lines (default is 1).
|
||||||
|
|
||||||
|
The plugin also adds four public methods:
|
||||||
|
|
||||||
|
- setCrosshair(pos)
|
||||||
|
|
||||||
|
Set the position of the crosshair. Note that this is cleared if
|
||||||
|
the user moves the mouse. "pos" should be on the form { x: xpos,
|
||||||
|
y: ypos } (or x2 and y2 if you're using the secondary axes), which
|
||||||
|
is coincidentally the same format as what you get from a "plothover"
|
||||||
|
event. If "pos" is null, the crosshair is cleared.
|
||||||
|
|
||||||
|
- clearCrosshair()
|
||||||
|
|
||||||
|
Clear the crosshair.
|
||||||
|
|
||||||
|
- lockCrosshair(pos)
|
||||||
|
|
||||||
|
Cause the crosshair to lock to the current location, no longer
|
||||||
|
updating if the user moves the mouse. Optionally supply a position
|
||||||
|
(passed on to setCrosshair()) to move it to.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } };
|
||||||
|
$("#graph").bind("plothover", function (evt, position, item) {
|
||||||
|
if (item) {
|
||||||
|
// Lock the crosshair to the data point being hovered
|
||||||
|
myFlot.lockCrosshair({ x: item.datapoint[0], y: item.datapoint[1] });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Return normal crosshair operation
|
||||||
|
myFlot.unlockCrosshair();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
- unlockCrosshair()
|
||||||
|
|
||||||
|
Free the crosshair to move again after locking it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
var options = {
|
||||||
|
crosshair: {
|
||||||
|
mode: null, // one of null, "x", "y" or "xy",
|
||||||
|
color: "rgba(170, 0, 0, 0.80)",
|
||||||
|
lineWidth: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
// position of crosshair in pixels
|
||||||
|
var crosshair = { x: -1, y: -1, locked: false };
|
||||||
|
|
||||||
|
plot.setCrosshair = function setCrosshair(pos) {
|
||||||
|
if (!pos)
|
||||||
|
crosshair.x = -1;
|
||||||
|
else {
|
||||||
|
var axes = plot.getAxes();
|
||||||
|
|
||||||
|
crosshair.x = Math.max(0, Math.min(pos.x != null ? axes.xaxis.p2c(pos.x) : axes.x2axis.p2c(pos.x2), plot.width()));
|
||||||
|
crosshair.y = Math.max(0, Math.min(pos.y != null ? axes.yaxis.p2c(pos.y) : axes.y2axis.p2c(pos.y2), plot.height()));
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
};
|
||||||
|
|
||||||
|
plot.clearCrosshair = plot.setCrosshair; // passes null for pos
|
||||||
|
|
||||||
|
plot.lockCrosshair = function lockCrosshair(pos) {
|
||||||
|
if (pos)
|
||||||
|
plot.setCrosshair(pos);
|
||||||
|
crosshair.locked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.unlockCrosshair = function unlockCrosshair() {
|
||||||
|
crosshair.locked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.hooks.bindEvents.push(function (plot, eventHolder) {
|
||||||
|
if (!plot.getOptions().crosshair.mode)
|
||||||
|
return;
|
||||||
|
|
||||||
|
eventHolder.mouseout(function () {
|
||||||
|
if (crosshair.x != -1) {
|
||||||
|
crosshair.x = -1;
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventHolder.mousemove(function (e) {
|
||||||
|
if (plot.getSelection && plot.getSelection()) {
|
||||||
|
crosshair.x = -1; // hide the crosshair while selecting
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crosshair.locked)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var offset = plot.offset();
|
||||||
|
crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
|
||||||
|
crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
plot.hooks.drawOverlay.push(function (plot, ctx) {
|
||||||
|
var c = plot.getOptions().crosshair;
|
||||||
|
if (!c.mode)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var plotOffset = plot.getPlotOffset();
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(plotOffset.left, plotOffset.top);
|
||||||
|
|
||||||
|
if (crosshair.x != -1) {
|
||||||
|
ctx.strokeStyle = c.color;
|
||||||
|
ctx.lineWidth = c.lineWidth;
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
if (c.mode.indexOf("x") != -1) {
|
||||||
|
ctx.moveTo(crosshair.x, 0);
|
||||||
|
ctx.lineTo(crosshair.x, plot.height());
|
||||||
|
}
|
||||||
|
if (c.mode.indexOf("y") != -1) {
|
||||||
|
ctx.moveTo(0, crosshair.y);
|
||||||
|
ctx.lineTo(plot.width(), crosshair.y);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'crosshair',
|
||||||
|
version: '1.0'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
1
browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.crosshair.min.js
поставляемый
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(function(B){var A={crosshair:{mode:null,color:"rgba(170, 0, 0, 0.80)",lineWidth:1}};function C(G){var H={x:-1,y:-1,locked:false};G.setCrosshair=function D(J){if(!J){H.x=-1}else{var I=G.getAxes();H.x=Math.max(0,Math.min(J.x!=null?I.xaxis.p2c(J.x):I.x2axis.p2c(J.x2),G.width()));H.y=Math.max(0,Math.min(J.y!=null?I.yaxis.p2c(J.y):I.y2axis.p2c(J.y2),G.height()))}G.triggerRedrawOverlay()};G.clearCrosshair=G.setCrosshair;G.lockCrosshair=function E(I){if(I){G.setCrosshair(I)}H.locked=true};G.unlockCrosshair=function F(){H.locked=false};G.hooks.bindEvents.push(function(J,I){if(!J.getOptions().crosshair.mode){return }I.mouseout(function(){if(H.x!=-1){H.x=-1;J.triggerRedrawOverlay()}});I.mousemove(function(K){if(J.getSelection&&J.getSelection()){H.x=-1;return }if(H.locked){return }var L=J.offset();H.x=Math.max(0,Math.min(K.pageX-L.left,J.width()));H.y=Math.max(0,Math.min(K.pageY-L.top,J.height()));J.triggerRedrawOverlay()})});G.hooks.drawOverlay.push(function(K,I){var L=K.getOptions().crosshair;if(!L.mode){return }var J=K.getPlotOffset();I.save();I.translate(J.left,J.top);if(H.x!=-1){I.strokeStyle=L.color;I.lineWidth=L.lineWidth;I.lineJoin="round";I.beginPath();if(L.mode.indexOf("x")!=-1){I.moveTo(H.x,0);I.lineTo(H.x,K.height())}if(L.mode.indexOf("y")!=-1){I.moveTo(0,H.y);I.lineTo(K.width(),H.y)}I.stroke()}I.restore()})}B.plot.plugins.push({init:C,options:A,name:"crosshair",version:"1.0"})})(jQuery);
|
|
@ -0,0 +1,237 @@
|
||||||
|
/*
|
||||||
|
Flot plugin for plotting images, e.g. useful for putting ticks on a
|
||||||
|
prerendered complex visualization.
|
||||||
|
|
||||||
|
The data syntax is [[image, x1, y1, x2, y2], ...] where (x1, y1) and
|
||||||
|
(x2, y2) are where you intend the two opposite corners of the image to
|
||||||
|
end up in the plot. Image must be a fully loaded Javascript image (you
|
||||||
|
can make one with new Image()). If the image is not complete, it's
|
||||||
|
skipped when plotting.
|
||||||
|
|
||||||
|
There are two helpers included for retrieving images. The easiest work
|
||||||
|
the way that you put in URLs instead of images in the data (like
|
||||||
|
["myimage.png", 0, 0, 10, 10]), then call $.plot.image.loadData(data,
|
||||||
|
options, callback) where data and options are the same as you pass in
|
||||||
|
to $.plot. This loads the images, replaces the URLs in the data with
|
||||||
|
the corresponding images and calls "callback" when all images are
|
||||||
|
loaded (or failed loading). In the callback, you can then call $.plot
|
||||||
|
with the data set. See the included example.
|
||||||
|
|
||||||
|
A more low-level helper, $.plot.image.load(urls, callback) is also
|
||||||
|
included. Given a list of URLs, it calls callback with an object
|
||||||
|
mapping from URL to Image object when all images are loaded or have
|
||||||
|
failed loading.
|
||||||
|
|
||||||
|
Options for the plugin are
|
||||||
|
|
||||||
|
series: {
|
||||||
|
images: {
|
||||||
|
show: boolean
|
||||||
|
anchor: "corner" or "center"
|
||||||
|
alpha: [0,1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
which can be specified for a specific series
|
||||||
|
|
||||||
|
$.plot($("#placeholder"), [{ data: [ ... ], images: { ... } ])
|
||||||
|
|
||||||
|
Note that because the data format is different from usual data points,
|
||||||
|
you can't use images with anything else in a specific data series.
|
||||||
|
|
||||||
|
Setting "anchor" to "center" causes the pixels in the image to be
|
||||||
|
anchored at the corner pixel centers inside of at the pixel corners,
|
||||||
|
effectively letting half a pixel stick out to each side in the plot.
|
||||||
|
|
||||||
|
|
||||||
|
A possible future direction could be support for tiling for large
|
||||||
|
images (like Google Maps).
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
var options = {
|
||||||
|
series: {
|
||||||
|
images: {
|
||||||
|
show: false,
|
||||||
|
alpha: 1,
|
||||||
|
anchor: "corner" // or "center"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.plot.image = {};
|
||||||
|
|
||||||
|
$.plot.image.loadDataImages = function (series, options, callback) {
|
||||||
|
var urls = [], points = [];
|
||||||
|
|
||||||
|
var defaultShow = options.series.images.show;
|
||||||
|
|
||||||
|
$.each(series, function (i, s) {
|
||||||
|
if (!(defaultShow || s.images.show))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (s.data)
|
||||||
|
s = s.data;
|
||||||
|
|
||||||
|
$.each(s, function (i, p) {
|
||||||
|
if (typeof p[0] == "string") {
|
||||||
|
urls.push(p[0]);
|
||||||
|
points.push(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$.plot.image.load(urls, function (loadedImages) {
|
||||||
|
$.each(points, function (i, p) {
|
||||||
|
var url = p[0];
|
||||||
|
if (loadedImages[url])
|
||||||
|
p[0] = loadedImages[url];
|
||||||
|
});
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.image.load = function (urls, callback) {
|
||||||
|
var missing = urls.length, loaded = {};
|
||||||
|
if (missing == 0)
|
||||||
|
callback({});
|
||||||
|
|
||||||
|
$.each(urls, function (i, url) {
|
||||||
|
var handler = function () {
|
||||||
|
--missing;
|
||||||
|
|
||||||
|
loaded[url] = this;
|
||||||
|
|
||||||
|
if (missing == 0)
|
||||||
|
callback(loaded);
|
||||||
|
};
|
||||||
|
|
||||||
|
$('<img />').load(handler).error(handler).attr('src', url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw(plot, ctx) {
|
||||||
|
var plotOffset = plot.getPlotOffset();
|
||||||
|
|
||||||
|
$.each(plot.getData(), function (i, series) {
|
||||||
|
var points = series.datapoints.points,
|
||||||
|
ps = series.datapoints.pointsize;
|
||||||
|
|
||||||
|
for (var i = 0; i < points.length; i += ps) {
|
||||||
|
var img = points[i],
|
||||||
|
x1 = points[i + 1], y1 = points[i + 2],
|
||||||
|
x2 = points[i + 3], y2 = points[i + 4],
|
||||||
|
xaxis = series.xaxis, yaxis = series.yaxis,
|
||||||
|
tmp;
|
||||||
|
|
||||||
|
// actually we should check img.complete, but it
|
||||||
|
// appears to be a somewhat unreliable indicator in
|
||||||
|
// IE6 (false even after load event)
|
||||||
|
if (!img || img.width <= 0 || img.height <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (x1 > x2) {
|
||||||
|
tmp = x2;
|
||||||
|
x2 = x1;
|
||||||
|
x1 = tmp;
|
||||||
|
}
|
||||||
|
if (y1 > y2) {
|
||||||
|
tmp = y2;
|
||||||
|
y2 = y1;
|
||||||
|
y1 = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the anchor is at the center of the pixel, expand the
|
||||||
|
// image by 1/2 pixel in each direction
|
||||||
|
if (series.images.anchor == "center") {
|
||||||
|
tmp = 0.5 * (x2-x1) / (img.width - 1);
|
||||||
|
x1 -= tmp;
|
||||||
|
x2 += tmp;
|
||||||
|
tmp = 0.5 * (y2-y1) / (img.height - 1);
|
||||||
|
y1 -= tmp;
|
||||||
|
y2 += tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clip
|
||||||
|
if (x1 == x2 || y1 == y2 ||
|
||||||
|
x1 >= xaxis.max || x2 <= xaxis.min ||
|
||||||
|
y1 >= yaxis.max || y2 <= yaxis.min)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height;
|
||||||
|
if (x1 < xaxis.min) {
|
||||||
|
sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1);
|
||||||
|
x1 = xaxis.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x2 > xaxis.max) {
|
||||||
|
sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1);
|
||||||
|
x2 = xaxis.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y1 < yaxis.min) {
|
||||||
|
sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1);
|
||||||
|
y1 = yaxis.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y2 > yaxis.max) {
|
||||||
|
sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1);
|
||||||
|
y2 = yaxis.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
x1 = xaxis.p2c(x1);
|
||||||
|
x2 = xaxis.p2c(x2);
|
||||||
|
y1 = yaxis.p2c(y1);
|
||||||
|
y2 = yaxis.p2c(y2);
|
||||||
|
|
||||||
|
// the transformation may have swapped us
|
||||||
|
if (x1 > x2) {
|
||||||
|
tmp = x2;
|
||||||
|
x2 = x1;
|
||||||
|
x1 = tmp;
|
||||||
|
}
|
||||||
|
if (y1 > y2) {
|
||||||
|
tmp = y2;
|
||||||
|
y2 = y1;
|
||||||
|
y1 = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp = ctx.globalAlpha;
|
||||||
|
ctx.globalAlpha *= series.images.alpha;
|
||||||
|
ctx.drawImage(img,
|
||||||
|
sx1, sy1, sx2 - sx1, sy2 - sy1,
|
||||||
|
x1 + plotOffset.left, y1 + plotOffset.top,
|
||||||
|
x2 - x1, y2 - y1);
|
||||||
|
ctx.globalAlpha = tmp;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processRawData(plot, series, data, datapoints) {
|
||||||
|
if (!series.images.show)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// format is Image, x1, y1, x2, y2 (opposite corners)
|
||||||
|
datapoints.format = [
|
||||||
|
{ required: true },
|
||||||
|
{ x: true, number: true, required: true },
|
||||||
|
{ y: true, number: true, required: true },
|
||||||
|
{ x: true, number: true, required: true },
|
||||||
|
{ y: true, number: true, required: true }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
plot.hooks.processRawData.push(processRawData);
|
||||||
|
plot.hooks.draw.push(draw);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'image',
|
||||||
|
version: '1.1'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
1
browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.image.min.js
поставляемый
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(function(D){var B={series:{images:{show:false,alpha:1,anchor:"corner"}}};D.plot.image={};D.plot.image.loadDataImages=function(G,F,K){var J=[],H=[];var I=F.series.images.show;D.each(G,function(L,M){if(!(I||M.images.show)){return }if(M.data){M=M.data}D.each(M,function(N,O){if(typeof O[0]=="string"){J.push(O[0]);H.push(O)}})});D.plot.image.load(J,function(L){D.each(H,function(N,O){var M=O[0];if(L[M]){O[0]=L[M]}});K()})};D.plot.image.load=function(H,I){var G=H.length,F={};if(G==0){I({})}D.each(H,function(K,J){var L=function(){--G;F[J]=this;if(G==0){I(F)}};D("<img />").load(L).error(L).attr("src",J)})};function A(H,F){var G=H.getPlotOffset();D.each(H.getData(),function(O,P){var X=P.datapoints.points,I=P.datapoints.pointsize;for(var O=0;O<X.length;O+=I){var Q=X[O],M=X[O+1],V=X[O+2],K=X[O+3],T=X[O+4],W=P.xaxis,S=P.yaxis,N;if(!Q||Q.width<=0||Q.height<=0){continue}if(M>K){N=K;K=M;M=N}if(V>T){N=T;T=V;V=N}if(P.images.anchor=="center"){N=0.5*(K-M)/(Q.width-1);M-=N;K+=N;N=0.5*(T-V)/(Q.height-1);V-=N;T+=N}if(M==K||V==T||M>=W.max||K<=W.min||V>=S.max||T<=S.min){continue}var L=0,U=0,J=Q.width,R=Q.height;if(M<W.min){L+=(J-L)*(W.min-M)/(K-M);M=W.min}if(K>W.max){J+=(J-L)*(W.max-K)/(K-M);K=W.max}if(V<S.min){R+=(U-R)*(S.min-V)/(T-V);V=S.min}if(T>S.max){U+=(U-R)*(S.max-T)/(T-V);T=S.max}M=W.p2c(M);K=W.p2c(K);V=S.p2c(V);T=S.p2c(T);if(M>K){N=K;K=M;M=N}if(V>T){N=T;T=V;V=N}N=F.globalAlpha;F.globalAlpha*=P.images.alpha;F.drawImage(Q,L,U,J-L,R-U,M+G.left,V+G.top,K-M,T-V);F.globalAlpha=N}})}function C(I,F,G,H){if(!F.images.show){return }H.format=[{required:true},{x:true,number:true,required:true},{y:true,number:true,required:true},{x:true,number:true,required:true},{y:true,number:true,required:true}]}function E(F){F.hooks.processRawData.push(C);F.hooks.draw.push(A)}D.plot.plugins.push({init:E,options:B,name:"image",version:"1.1"})})(jQuery);
|
1
browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.min.js
поставляемый
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
/*
|
||||||
|
Flot plugin for adding panning and zooming capabilities to a plot.
|
||||||
|
|
||||||
|
The default behaviour is double click and scrollwheel up/down to zoom
|
||||||
|
in, drag to pan. The plugin defines plot.zoom({ center }),
|
||||||
|
plot.zoomOut() and plot.pan(offset) so you easily can add custom
|
||||||
|
controls. It also fires a "plotpan" and "plotzoom" event when
|
||||||
|
something happens, useful for synchronizing plots.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
plot = $.plot(...);
|
||||||
|
|
||||||
|
// zoom default amount in on the pixel (100, 200)
|
||||||
|
plot.zoom({ center: { left: 10, top: 20 } });
|
||||||
|
|
||||||
|
// zoom out again
|
||||||
|
plot.zoomOut({ center: { left: 10, top: 20 } });
|
||||||
|
|
||||||
|
// pan 100 pixels to the left and 20 down
|
||||||
|
plot.pan({ left: -100, top: 20 })
|
||||||
|
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
zoom: {
|
||||||
|
interactive: false
|
||||||
|
trigger: "dblclick" // or "click" for single click
|
||||||
|
amount: 1.5 // 2 = 200% (zoom in), 0.5 = 50% (zoom out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pan: {
|
||||||
|
interactive: false
|
||||||
|
}
|
||||||
|
|
||||||
|
xaxis, yaxis, x2axis, y2axis: {
|
||||||
|
zoomRange: null // or [number, number] (min range, max range)
|
||||||
|
panRange: null // or [number, number] (min, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
"interactive" enables the built-in drag/click behaviour. "amount" is
|
||||||
|
the amount to zoom the viewport relative to the current range, so 1 is
|
||||||
|
100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out).
|
||||||
|
|
||||||
|
"zoomRange" is the interval in which zooming can happen, e.g. with
|
||||||
|
zoomRange: [1, 100] the zoom will never scale the axis so that the
|
||||||
|
difference between min and max is smaller than 1 or larger than 100.
|
||||||
|
You can set either of them to null to ignore.
|
||||||
|
|
||||||
|
"panRange" confines the panning to stay within a range, e.g. with
|
||||||
|
panRange: [-10, 20] panning stops at -10 in one end and at 20 in the
|
||||||
|
other. Either can be null.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// First two dependencies, jquery.event.drag.js and
|
||||||
|
// jquery.mousewheel.js, we put them inline here to save people the
|
||||||
|
// effort of downloading them.
|
||||||
|
|
||||||
|
/*
|
||||||
|
jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com)
|
||||||
|
Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt
|
||||||
|
*/
|
||||||
|
(function(E){E.fn.drag=function(L,K,J){if(K){this.bind("dragstart",L)}if(J){this.bind("dragend",J)}return !L?this.trigger("drag"):this.bind("drag",K?K:L)};var A=E.event,B=A.special,F=B.drag={not:":input",distance:0,which:1,dragging:false,setup:function(J){J=E.extend({distance:F.distance,which:F.which,not:F.not},J||{});J.distance=I(J.distance);A.add(this,"mousedown",H,J);if(this.attachEvent){this.attachEvent("ondragstart",D)}},teardown:function(){A.remove(this,"mousedown",H);if(this===F.dragging){F.dragging=F.proxy=false}G(this,true);if(this.detachEvent){this.detachEvent("ondragstart",D)}}};B.dragstart=B.dragend={setup:function(){},teardown:function(){}};function H(L){var K=this,J,M=L.data||{};if(M.elem){K=L.dragTarget=M.elem;L.dragProxy=F.proxy||K;L.cursorOffsetX=M.pageX-M.left;L.cursorOffsetY=M.pageY-M.top;L.offsetX=L.pageX-L.cursorOffsetX;L.offsetY=L.pageY-L.cursorOffsetY}else{if(F.dragging||(M.which>0&&L.which!=M.which)||E(L.target).is(M.not)){return }}switch(L.type){case"mousedown":E.extend(M,E(K).offset(),{elem:K,target:L.target,pageX:L.pageX,pageY:L.pageY});A.add(document,"mousemove mouseup",H,M);G(K,false);F.dragging=null;return false;case !F.dragging&&"mousemove":if(I(L.pageX-M.pageX)+I(L.pageY-M.pageY)<M.distance){break}L.target=M.target;J=C(L,"dragstart",K);if(J!==false){F.dragging=K;F.proxy=L.dragProxy=E(J||K)[0]}case"mousemove":if(F.dragging){J=C(L,"drag",K);if(B.drop){B.drop.allowed=(J!==false);B.drop.handler(L)}if(J!==false){break}L.type="mouseup"}case"mouseup":A.remove(document,"mousemove mouseup",H);if(F.dragging){if(B.drop){B.drop.handler(L)}C(L,"dragend",K)}G(K,true);F.dragging=F.proxy=M.elem=false;break}return true}function C(M,K,L){M.type=K;var J=E.event.handle.call(L,M);return J===false?false:J||M.result}function I(J){return Math.pow(J,2)}function D(){return(F.dragging===false)}function G(K,J){if(!K){return }K.unselectable=J?"off":"on";K.onselectstart=function(){return J};if(K.style){K.style.MozUserSelect=J?"":"none"}}})(jQuery);
|
||||||
|
|
||||||
|
|
||||||
|
/* jquery.mousewheel.min.js
|
||||||
|
* Copyright (c) 2009 Brandon Aaron (http://brandonaaron.net)
|
||||||
|
* Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
|
||||||
|
* and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
|
||||||
|
* Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers.
|
||||||
|
* Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix.
|
||||||
|
*
|
||||||
|
* Version: 3.0.2
|
||||||
|
*
|
||||||
|
* Requires: 1.2.2+
|
||||||
|
*/
|
||||||
|
(function(c){var a=["DOMMouseScroll","mousewheel"];c.event.special.mousewheel={setup:function(){if(this.addEventListener){for(var d=a.length;d;){this.addEventListener(a[--d],b,false)}}else{this.onmousewheel=b}},teardown:function(){if(this.removeEventListener){for(var d=a.length;d;){this.removeEventListener(a[--d],b,false)}}else{this.onmousewheel=null}}};c.fn.extend({mousewheel:function(d){return d?this.bind("mousewheel",d):this.trigger("mousewheel")},unmousewheel:function(d){return this.unbind("mousewheel",d)}});function b(f){var d=[].slice.call(arguments,1),g=0,e=true;f=c.event.fix(f||window.event);f.type="mousewheel";if(f.wheelDelta){g=f.wheelDelta/120}if(f.detail){g=-f.detail/3}d.unshift(f,g);return c.event.handle.apply(this,d)}})(jQuery);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
var options = {
|
||||||
|
xaxis: {
|
||||||
|
zoomRange: null, // or [number, number] (min range, max range)
|
||||||
|
panRange: null // or [number, number] (min, max)
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
interactive: false,
|
||||||
|
trigger: "dblclick", // or "click" for single click
|
||||||
|
amount: 1.5 // how much to zoom relative to current position, 2 = 200% (zoom in), 0.5 = 50% (zoom out)
|
||||||
|
},
|
||||||
|
pan: {
|
||||||
|
interactive: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
function bindEvents(plot, eventHolder) {
|
||||||
|
var o = plot.getOptions();
|
||||||
|
if (o.zoom.interactive) {
|
||||||
|
function clickHandler(e, zoomOut) {
|
||||||
|
var c = plot.offset();
|
||||||
|
c.left = e.pageX - c.left;
|
||||||
|
c.top = e.pageY - c.top;
|
||||||
|
if (zoomOut)
|
||||||
|
plot.zoomOut({ center: c });
|
||||||
|
else
|
||||||
|
plot.zoom({ center: c });
|
||||||
|
}
|
||||||
|
|
||||||
|
eventHolder[o.zoom.trigger](clickHandler);
|
||||||
|
|
||||||
|
eventHolder.mousewheel(function (e, delta) {
|
||||||
|
clickHandler(e, delta < 0);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (o.pan.interactive) {
|
||||||
|
var prevCursor = 'default', pageX = 0, pageY = 0;
|
||||||
|
|
||||||
|
eventHolder.bind("dragstart", { distance: 10 }, function (e) {
|
||||||
|
if (e.which != 1) // only accept left-click
|
||||||
|
return false;
|
||||||
|
eventHolderCursor = eventHolder.css('cursor');
|
||||||
|
eventHolder.css('cursor', 'move');
|
||||||
|
pageX = e.pageX;
|
||||||
|
pageY = e.pageY;
|
||||||
|
});
|
||||||
|
eventHolder.bind("drag", function (e) {
|
||||||
|
// unused at the moment, but we need it here to
|
||||||
|
// trigger the dragstart/dragend events
|
||||||
|
});
|
||||||
|
eventHolder.bind("dragend", function (e) {
|
||||||
|
eventHolder.css('cursor', prevCursor);
|
||||||
|
plot.pan({ left: pageX - e.pageX,
|
||||||
|
top: pageY - e.pageY });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.zoomOut = function (args) {
|
||||||
|
if (!args)
|
||||||
|
args = {};
|
||||||
|
|
||||||
|
if (!args.amount)
|
||||||
|
args.amount = plot.getOptions().zoom.amount
|
||||||
|
|
||||||
|
args.amount = 1 / args.amount;
|
||||||
|
plot.zoom(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.zoom = function (args) {
|
||||||
|
if (!args)
|
||||||
|
args = {};
|
||||||
|
|
||||||
|
var axes = plot.getAxes(),
|
||||||
|
options = plot.getOptions(),
|
||||||
|
c = args.center,
|
||||||
|
amount = args.amount ? args.amount : options.zoom.amount,
|
||||||
|
w = plot.width(), h = plot.height();
|
||||||
|
|
||||||
|
if (!c)
|
||||||
|
c = { left: w / 2, top: h / 2 };
|
||||||
|
|
||||||
|
var xf = c.left / w,
|
||||||
|
x1 = c.left - xf * w / amount,
|
||||||
|
x2 = c.left + (1 - xf) * w / amount,
|
||||||
|
yf = c.top / h,
|
||||||
|
y1 = c.top - yf * h / amount,
|
||||||
|
y2 = c.top + (1 - yf) * h / amount;
|
||||||
|
|
||||||
|
function scaleAxis(min, max, name) {
|
||||||
|
var axis = axes[name],
|
||||||
|
axisOptions = options[name];
|
||||||
|
|
||||||
|
if (!axis.used)
|
||||||
|
return;
|
||||||
|
|
||||||
|
min = axis.c2p(min);
|
||||||
|
max = axis.c2p(max);
|
||||||
|
if (max < min) { // make sure min < max
|
||||||
|
var tmp = min
|
||||||
|
min = max;
|
||||||
|
max = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
var range = max - min, zr = axisOptions.zoomRange;
|
||||||
|
if (zr &&
|
||||||
|
((zr[0] != null && range < zr[0]) ||
|
||||||
|
(zr[1] != null && range > zr[1])))
|
||||||
|
return;
|
||||||
|
|
||||||
|
axisOptions.min = min;
|
||||||
|
axisOptions.max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleAxis(x1, x2, 'xaxis');
|
||||||
|
scaleAxis(x1, x2, 'x2axis');
|
||||||
|
scaleAxis(y1, y2, 'yaxis');
|
||||||
|
scaleAxis(y1, y2, 'y2axis');
|
||||||
|
|
||||||
|
plot.setupGrid();
|
||||||
|
plot.draw();
|
||||||
|
|
||||||
|
if (!args.preventEvent)
|
||||||
|
plot.getPlaceholder().trigger("plotzoom", [ plot ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.pan = function (args) {
|
||||||
|
var l = +args.left, t = +args.top,
|
||||||
|
axes = plot.getAxes(), options = plot.getOptions();
|
||||||
|
|
||||||
|
if (isNaN(l))
|
||||||
|
l = 0;
|
||||||
|
if (isNaN(t))
|
||||||
|
t = 0;
|
||||||
|
|
||||||
|
function panAxis(delta, name) {
|
||||||
|
var axis = axes[name],
|
||||||
|
axisOptions = options[name],
|
||||||
|
min, max;
|
||||||
|
|
||||||
|
if (!axis.used)
|
||||||
|
return;
|
||||||
|
|
||||||
|
min = axis.c2p(axis.p2c(axis.min) + delta),
|
||||||
|
max = axis.c2p(axis.p2c(axis.max) + delta);
|
||||||
|
|
||||||
|
var pr = axisOptions.panRange;
|
||||||
|
if (pr) {
|
||||||
|
// check whether we hit the wall
|
||||||
|
if (pr[0] != null && pr[0] > min) {
|
||||||
|
delta = pr[0] - min;
|
||||||
|
min += delta;
|
||||||
|
max += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pr[1] != null && pr[1] < max) {
|
||||||
|
delta = pr[1] - max;
|
||||||
|
min += delta;
|
||||||
|
max += delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
axisOptions.min = min;
|
||||||
|
axisOptions.max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
panAxis(l, 'xaxis');
|
||||||
|
panAxis(l, 'x2axis');
|
||||||
|
panAxis(t, 'yaxis');
|
||||||
|
panAxis(t, 'y2axis');
|
||||||
|
|
||||||
|
plot.setupGrid();
|
||||||
|
plot.draw();
|
||||||
|
|
||||||
|
if (!args.preventEvent)
|
||||||
|
plot.getPlaceholder().trigger("plotpan", [ plot ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.hooks.bindEvents.push(bindEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'navigate',
|
||||||
|
version: '1.1'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
1
browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.navigate.min.js
поставляемый
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(function(R){R.fn.drag=function(A,B,C){if(B){this.bind("dragstart",A)}if(C){this.bind("dragend",C)}return !A?this.trigger("drag"):this.bind("drag",B?B:A)};var M=R.event,L=M.special,Q=L.drag={not:":input",distance:0,which:1,dragging:false,setup:function(A){A=R.extend({distance:Q.distance,which:Q.which,not:Q.not},A||{});A.distance=N(A.distance);M.add(this,"mousedown",O,A);if(this.attachEvent){this.attachEvent("ondragstart",J)}},teardown:function(){M.remove(this,"mousedown",O);if(this===Q.dragging){Q.dragging=Q.proxy=false}P(this,true);if(this.detachEvent){this.detachEvent("ondragstart",J)}}};L.dragstart=L.dragend={setup:function(){},teardown:function(){}};function O(A){var B=this,C,D=A.data||{};if(D.elem){B=A.dragTarget=D.elem;A.dragProxy=Q.proxy||B;A.cursorOffsetX=D.pageX-D.left;A.cursorOffsetY=D.pageY-D.top;A.offsetX=A.pageX-A.cursorOffsetX;A.offsetY=A.pageY-A.cursorOffsetY}else{if(Q.dragging||(D.which>0&&A.which!=D.which)||R(A.target).is(D.not)){return }}switch(A.type){case"mousedown":R.extend(D,R(B).offset(),{elem:B,target:A.target,pageX:A.pageX,pageY:A.pageY});M.add(document,"mousemove mouseup",O,D);P(B,false);Q.dragging=null;return false;case !Q.dragging&&"mousemove":if(N(A.pageX-D.pageX)+N(A.pageY-D.pageY)<D.distance){break}A.target=D.target;C=K(A,"dragstart",B);if(C!==false){Q.dragging=B;Q.proxy=A.dragProxy=R(C||B)[0]}case"mousemove":if(Q.dragging){C=K(A,"drag",B);if(L.drop){L.drop.allowed=(C!==false);L.drop.handler(A)}if(C!==false){break}A.type="mouseup"}case"mouseup":M.remove(document,"mousemove mouseup",O);if(Q.dragging){if(L.drop){L.drop.handler(A)}K(A,"dragend",B)}P(B,true);Q.dragging=Q.proxy=D.elem=false;break}return true}function K(D,B,A){D.type=B;var C=R.event.handle.call(A,D);return C===false?false:C||D.result}function N(A){return Math.pow(A,2)}function J(){return(Q.dragging===false)}function P(A,B){if(!A){return }A.unselectable=B?"off":"on";A.onselectstart=function(){return B};if(A.style){A.style.MozUserSelect=B?"":"none"}}})(jQuery);(function(C){var B=["DOMMouseScroll","mousewheel"];C.event.special.mousewheel={setup:function(){if(this.addEventListener){for(var D=B.length;D;){this.addEventListener(B[--D],A,false)}}else{this.onmousewheel=A}},teardown:function(){if(this.removeEventListener){for(var D=B.length;D;){this.removeEventListener(B[--D],A,false)}}else{this.onmousewheel=null}}};C.fn.extend({mousewheel:function(D){return D?this.bind("mousewheel",D):this.trigger("mousewheel")},unmousewheel:function(D){return this.unbind("mousewheel",D)}});function A(E){var G=[].slice.call(arguments,1),D=0,F=true;E=C.event.fix(E||window.event);E.type="mousewheel";if(E.wheelDelta){D=E.wheelDelta/120}if(E.detail){D=-E.detail/3}G.unshift(E,D);return C.event.handle.apply(this,G)}})(jQuery);(function(B){var A={xaxis:{zoomRange:null,panRange:null},zoom:{interactive:false,trigger:"dblclick",amount:1.5},pan:{interactive:false}};function C(D){function E(J,F){var K=J.getOptions();if(K.zoom.interactive){function L(N,M){var O=J.offset();O.left=N.pageX-O.left;O.top=N.pageY-O.top;if(M){J.zoomOut({center:O})}else{J.zoom({center:O})}}F[K.zoom.trigger](L);F.mousewheel(function(M,N){L(M,N<0);return false})}if(K.pan.interactive){var I="default",H=0,G=0;F.bind("dragstart",{distance:10},function(M){if(M.which!=1){return false}eventHolderCursor=F.css("cursor");F.css("cursor","move");H=M.pageX;G=M.pageY});F.bind("drag",function(M){});F.bind("dragend",function(M){F.css("cursor",I);J.pan({left:H-M.pageX,top:G-M.pageY})})}}D.zoomOut=function(F){if(!F){F={}}if(!F.amount){F.amount=D.getOptions().zoom.amount}F.amount=1/F.amount;D.zoom(F)};D.zoom=function(M){if(!M){M={}}var L=D.getAxes(),S=D.getOptions(),N=M.center,J=M.amount?M.amount:S.zoom.amount,R=D.width(),I=D.height();if(!N){N={left:R/2,top:I/2}}var Q=N.left/R,G=N.left-Q*R/J,F=N.left+(1-Q)*R/J,H=N.top/I,P=N.top-H*I/J,O=N.top+(1-H)*I/J;function K(X,T,V){var Y=L[V],a=S[V];if(!Y.used){return }X=Y.c2p(X);T=Y.c2p(T);if(T<X){var W=X;X=T;T=W}var U=T-X,Z=a.zoomRange;if(Z&&((Z[0]!=null&&U<Z[0])||(Z[1]!=null&&U>Z[1]))){return }a.min=X;a.max=T}K(G,F,"xaxis");K(G,F,"x2axis");K(P,O,"yaxis");K(P,O,"y2axis");D.setupGrid();D.draw();if(!M.preventEvent){D.getPlaceholder().trigger("plotzoom",[D])}};D.pan=function(I){var F=+I.left,J=+I.top,K=D.getAxes(),H=D.getOptions();if(isNaN(F)){F=0}if(isNaN(J)){J=0}function G(R,M){var O=K[M],Q=H[M],N,L;if(!O.used){return }N=O.c2p(O.p2c(O.min)+R),L=O.c2p(O.p2c(O.max)+R);var P=Q.panRange;if(P){if(P[0]!=null&&P[0]>N){R=P[0]-N;N+=R;L+=R}if(P[1]!=null&&P[1]<L){R=P[1]-L;N+=R;L+=R}}Q.min=N;Q.max=L}G(F,"xaxis");G(F,"x2axis");G(J,"yaxis");G(J,"y2axis");D.setupGrid();D.draw();if(!I.preventEvent){D.getPlaceholder().trigger("plotpan",[D])}};D.hooks.bindEvents.push(E)}B.plot.plugins.push({init:C,options:A,name:"navigate",version:"1.1"})})(jQuery);
|
|
@ -0,0 +1,299 @@
|
||||||
|
/*
|
||||||
|
Flot plugin for selecting regions.
|
||||||
|
|
||||||
|
The plugin defines the following options:
|
||||||
|
|
||||||
|
selection: {
|
||||||
|
mode: null or "x" or "y" or "xy",
|
||||||
|
color: color
|
||||||
|
}
|
||||||
|
|
||||||
|
You enable selection support by setting the mode to one of "x", "y" or
|
||||||
|
"xy". In "x" mode, the user will only be able to specify the x range,
|
||||||
|
similarly for "y" mode. For "xy", the selection becomes a rectangle
|
||||||
|
where both ranges can be specified. "color" is color of the selection.
|
||||||
|
|
||||||
|
When selection support is enabled, a "plotselected" event will be emitted
|
||||||
|
on the DOM element you passed into the plot function. The event
|
||||||
|
handler gets one extra parameter with the ranges selected on the axes,
|
||||||
|
like this:
|
||||||
|
|
||||||
|
placeholder.bind("plotselected", function(event, ranges) {
|
||||||
|
alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to)
|
||||||
|
// similar for yaxis, secondary axes are in x2axis
|
||||||
|
// and y2axis if present
|
||||||
|
});
|
||||||
|
|
||||||
|
The "plotselected" event is only fired when the user has finished
|
||||||
|
making the selection. A "plotselecting" event is fired during the
|
||||||
|
process with the same parameters as the "plotselected" event, in case
|
||||||
|
you want to know what's happening while it's happening,
|
||||||
|
|
||||||
|
A "plotunselected" event with no arguments is emitted when the user
|
||||||
|
clicks the mouse to remove the selection.
|
||||||
|
|
||||||
|
The plugin allso adds the following methods to the plot object:
|
||||||
|
|
||||||
|
- setSelection(ranges, preventEvent)
|
||||||
|
|
||||||
|
Set the selection rectangle. The passed in ranges is on the same
|
||||||
|
form as returned in the "plotselected" event. If the selection
|
||||||
|
mode is "x", you should put in either an xaxis (or x2axis) object,
|
||||||
|
if the mode is "y" you need to put in an yaxis (or y2axis) object
|
||||||
|
and both xaxis/x2axis and yaxis/y2axis if the selection mode is
|
||||||
|
"xy", like this:
|
||||||
|
|
||||||
|
setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } });
|
||||||
|
|
||||||
|
setSelection will trigger the "plotselected" event when called. If
|
||||||
|
you don't want that to happen, e.g. if you're inside a
|
||||||
|
"plotselected" handler, pass true as the second parameter.
|
||||||
|
|
||||||
|
- clearSelection(preventEvent)
|
||||||
|
|
||||||
|
Clear the selection rectangle. Pass in true to avoid getting a
|
||||||
|
"plotunselected" event.
|
||||||
|
|
||||||
|
- getSelection()
|
||||||
|
|
||||||
|
Returns the current selection in the same format as the
|
||||||
|
"plotselected" event. If there's currently no selection, the
|
||||||
|
function returns null.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
function init(plot) {
|
||||||
|
var selection = {
|
||||||
|
first: { x: -1, y: -1}, second: { x: -1, y: -1},
|
||||||
|
show: false,
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// FIXME: The drag handling implemented here should be
|
||||||
|
// abstracted out, there's some similar code from a library in
|
||||||
|
// the navigation plugin, this should be massaged a bit to fit
|
||||||
|
// the Flot cases here better and reused. Doing this would
|
||||||
|
// make this plugin much slimmer.
|
||||||
|
var savedhandlers = {};
|
||||||
|
|
||||||
|
function onMouseMove(e) {
|
||||||
|
if (selection.active) {
|
||||||
|
plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]);
|
||||||
|
|
||||||
|
updateSelection(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseDown(e) {
|
||||||
|
if (e.which != 1) // only accept left-click
|
||||||
|
return;
|
||||||
|
|
||||||
|
// cancel out any text selections
|
||||||
|
document.body.focus();
|
||||||
|
|
||||||
|
// prevent text selection and drag in old-school browsers
|
||||||
|
if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) {
|
||||||
|
savedhandlers.onselectstart = document.onselectstart;
|
||||||
|
document.onselectstart = function () { return false; };
|
||||||
|
}
|
||||||
|
if (document.ondrag !== undefined && savedhandlers.ondrag == null) {
|
||||||
|
savedhandlers.ondrag = document.ondrag;
|
||||||
|
document.ondrag = function () { return false; };
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectionPos(selection.first, e);
|
||||||
|
|
||||||
|
selection.active = true;
|
||||||
|
|
||||||
|
$(document).one("mouseup", onMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp(e) {
|
||||||
|
// revert drag stuff for old-school browsers
|
||||||
|
if (document.onselectstart !== undefined)
|
||||||
|
document.onselectstart = savedhandlers.onselectstart;
|
||||||
|
if (document.ondrag !== undefined)
|
||||||
|
document.ondrag = savedhandlers.ondrag;
|
||||||
|
|
||||||
|
// no more draggy-dee-drag
|
||||||
|
selection.active = false;
|
||||||
|
updateSelection(e);
|
||||||
|
|
||||||
|
if (selectionIsSane())
|
||||||
|
triggerSelectedEvent();
|
||||||
|
else {
|
||||||
|
// this counts as a clear
|
||||||
|
plot.getPlaceholder().trigger("plotunselected", [ ]);
|
||||||
|
plot.getPlaceholder().trigger("plotselecting", [ null ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelection() {
|
||||||
|
if (!selectionIsSane())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var x1 = Math.min(selection.first.x, selection.second.x),
|
||||||
|
x2 = Math.max(selection.first.x, selection.second.x),
|
||||||
|
y1 = Math.max(selection.first.y, selection.second.y),
|
||||||
|
y2 = Math.min(selection.first.y, selection.second.y);
|
||||||
|
|
||||||
|
var r = {};
|
||||||
|
var axes = plot.getAxes();
|
||||||
|
if (axes.xaxis.used)
|
||||||
|
r.xaxis = { from: axes.xaxis.c2p(x1), to: axes.xaxis.c2p(x2) };
|
||||||
|
if (axes.x2axis.used)
|
||||||
|
r.x2axis = { from: axes.x2axis.c2p(x1), to: axes.x2axis.c2p(x2) };
|
||||||
|
if (axes.yaxis.used)
|
||||||
|
r.yaxis = { from: axes.yaxis.c2p(y1), to: axes.yaxis.c2p(y2) };
|
||||||
|
if (axes.y2axis.used)
|
||||||
|
r.y2axis = { from: axes.y2axis.c2p(y1), to: axes.y2axis.c2p(y2) };
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerSelectedEvent() {
|
||||||
|
var r = getSelection();
|
||||||
|
|
||||||
|
plot.getPlaceholder().trigger("plotselected", [ r ]);
|
||||||
|
|
||||||
|
// backwards-compat stuff, to be removed in future
|
||||||
|
var axes = plot.getAxes();
|
||||||
|
if (axes.xaxis.used && axes.yaxis.used)
|
||||||
|
plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(min, value, max) {
|
||||||
|
return value < min? min: (value > max? max: value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectionPos(pos, e) {
|
||||||
|
var o = plot.getOptions();
|
||||||
|
var offset = plot.getPlaceholder().offset();
|
||||||
|
var plotOffset = plot.getPlotOffset();
|
||||||
|
pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width());
|
||||||
|
pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height());
|
||||||
|
|
||||||
|
if (o.selection.mode == "y")
|
||||||
|
pos.x = pos == selection.first? 0: plot.width();
|
||||||
|
|
||||||
|
if (o.selection.mode == "x")
|
||||||
|
pos.y = pos == selection.first? 0: plot.height();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelection(pos) {
|
||||||
|
if (pos.pageX == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
setSelectionPos(selection.second, pos);
|
||||||
|
if (selectionIsSane()) {
|
||||||
|
selection.show = true;
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
clearSelection(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection(preventEvent) {
|
||||||
|
if (selection.show) {
|
||||||
|
selection.show = false;
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
if (!preventEvent)
|
||||||
|
plot.getPlaceholder().trigger("plotunselected", [ ]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelection(ranges, preventEvent) {
|
||||||
|
var axis, range, axes = plot.getAxes();
|
||||||
|
var o = plot.getOptions();
|
||||||
|
|
||||||
|
if (o.selection.mode == "y") {
|
||||||
|
selection.first.x = 0;
|
||||||
|
selection.second.x = plot.width();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
axis = ranges["xaxis"]? axes["xaxis"]: (ranges["x2axis"]? axes["x2axis"]: axes["xaxis"]);
|
||||||
|
range = ranges["xaxis"] || ranges["x2axis"] || { from:ranges["x1"], to:ranges["x2"] }
|
||||||
|
selection.first.x = axis.p2c(Math.min(range.from, range.to));
|
||||||
|
selection.second.x = axis.p2c(Math.max(range.from, range.to));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.selection.mode == "x") {
|
||||||
|
selection.first.y = 0;
|
||||||
|
selection.second.y = plot.height();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
axis = ranges["yaxis"]? axes["yaxis"]: (ranges["y2axis"]? axes["y2axis"]: axes["yaxis"]);
|
||||||
|
range = ranges["yaxis"] || ranges["y2axis"] || { from:ranges["y1"], to:ranges["y2"] }
|
||||||
|
selection.first.y = axis.p2c(Math.min(range.from, range.to));
|
||||||
|
selection.second.y = axis.p2c(Math.max(range.from, range.to));
|
||||||
|
}
|
||||||
|
|
||||||
|
selection.show = true;
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
if (!preventEvent)
|
||||||
|
triggerSelectedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectionIsSane() {
|
||||||
|
var minSize = 5;
|
||||||
|
return Math.abs(selection.second.x - selection.first.x) >= minSize &&
|
||||||
|
Math.abs(selection.second.y - selection.first.y) >= minSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.clearSelection = clearSelection;
|
||||||
|
plot.setSelection = setSelection;
|
||||||
|
plot.getSelection = getSelection;
|
||||||
|
|
||||||
|
plot.hooks.bindEvents.push(function(plot, eventHolder) {
|
||||||
|
var o = plot.getOptions();
|
||||||
|
if (o.selection.mode != null)
|
||||||
|
eventHolder.mousemove(onMouseMove);
|
||||||
|
|
||||||
|
if (o.selection.mode != null)
|
||||||
|
eventHolder.mousedown(onMouseDown);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
plot.hooks.drawOverlay.push(function (plot, ctx) {
|
||||||
|
// draw selection
|
||||||
|
if (selection.show && selectionIsSane()) {
|
||||||
|
var plotOffset = plot.getPlotOffset();
|
||||||
|
var o = plot.getOptions();
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(plotOffset.left, plotOffset.top);
|
||||||
|
|
||||||
|
var c = $.color.parse(o.selection.color);
|
||||||
|
|
||||||
|
ctx.strokeStyle = c.scale('a', 0.8).toString();
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
ctx.fillStyle = c.scale('a', 0.4).toString();
|
||||||
|
|
||||||
|
var x = Math.min(selection.first.x, selection.second.x),
|
||||||
|
y = Math.min(selection.first.y, selection.second.y),
|
||||||
|
w = Math.abs(selection.second.x - selection.first.x),
|
||||||
|
h = Math.abs(selection.second.y - selection.first.y);
|
||||||
|
|
||||||
|
ctx.fillRect(x, y, w, h);
|
||||||
|
ctx.strokeRect(x, y, w, h);
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: {
|
||||||
|
selection: {
|
||||||
|
mode: null, // one of null, "x", "y" or "xy"
|
||||||
|
color: "#e8cfac"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: 'selection',
|
||||||
|
version: '1.0'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
1
browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.selection.min.js
поставляемый
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(function(A){function B(J){var O={first:{x:-1,y:-1},second:{x:-1,y:-1},show:false,active:false};var L={};function D(Q){if(O.active){J.getPlaceholder().trigger("plotselecting",[F()]);K(Q)}}function M(Q){if(Q.which!=1){return }document.body.focus();if(document.onselectstart!==undefined&&L.onselectstart==null){L.onselectstart=document.onselectstart;document.onselectstart=function(){return false}}if(document.ondrag!==undefined&&L.ondrag==null){L.ondrag=document.ondrag;document.ondrag=function(){return false}}C(O.first,Q);O.active=true;A(document).one("mouseup",I)}function I(Q){if(document.onselectstart!==undefined){document.onselectstart=L.onselectstart}if(document.ondrag!==undefined){document.ondrag=L.ondrag}O.active=false;K(Q);if(E()){H()}else{J.getPlaceholder().trigger("plotunselected",[]);J.getPlaceholder().trigger("plotselecting",[null])}return false}function F(){if(!E()){return null}var R=Math.min(O.first.x,O.second.x),Q=Math.max(O.first.x,O.second.x),T=Math.max(O.first.y,O.second.y),S=Math.min(O.first.y,O.second.y);var U={};var V=J.getAxes();if(V.xaxis.used){U.xaxis={from:V.xaxis.c2p(R),to:V.xaxis.c2p(Q)}}if(V.x2axis.used){U.x2axis={from:V.x2axis.c2p(R),to:V.x2axis.c2p(Q)}}if(V.yaxis.used){U.yaxis={from:V.yaxis.c2p(T),to:V.yaxis.c2p(S)}}if(V.y2axis.used){U.y2axis={from:V.y2axis.c2p(T),to:V.y2axis.c2p(S)}}return U}function H(){var Q=F();J.getPlaceholder().trigger("plotselected",[Q]);var R=J.getAxes();if(R.xaxis.used&&R.yaxis.used){J.getPlaceholder().trigger("selected",[{x1:Q.xaxis.from,y1:Q.yaxis.from,x2:Q.xaxis.to,y2:Q.yaxis.to}])}}function G(R,S,Q){return S<R?R:(S>Q?Q:S)}function C(U,R){var T=J.getOptions();var S=J.getPlaceholder().offset();var Q=J.getPlotOffset();U.x=G(0,R.pageX-S.left-Q.left,J.width());U.y=G(0,R.pageY-S.top-Q.top,J.height());if(T.selection.mode=="y"){U.x=U==O.first?0:J.width()}if(T.selection.mode=="x"){U.y=U==O.first?0:J.height()}}function K(Q){if(Q.pageX==null){return }C(O.second,Q);if(E()){O.show=true;J.triggerRedrawOverlay()}else{P(true)}}function P(Q){if(O.show){O.show=false;J.triggerRedrawOverlay();if(!Q){J.getPlaceholder().trigger("plotunselected",[])}}}function N(R,Q){var T,S,U=J.getAxes();var V=J.getOptions();if(V.selection.mode=="y"){O.first.x=0;O.second.x=J.width()}else{T=R.xaxis?U.xaxis:(R.x2axis?U.x2axis:U.xaxis);S=R.xaxis||R.x2axis||{from:R.x1,to:R.x2};O.first.x=T.p2c(Math.min(S.from,S.to));O.second.x=T.p2c(Math.max(S.from,S.to))}if(V.selection.mode=="x"){O.first.y=0;O.second.y=J.height()}else{T=R.yaxis?U.yaxis:(R.y2axis?U.y2axis:U.yaxis);S=R.yaxis||R.y2axis||{from:R.y1,to:R.y2};O.first.y=T.p2c(Math.min(S.from,S.to));O.second.y=T.p2c(Math.max(S.from,S.to))}O.show=true;J.triggerRedrawOverlay();if(!Q){H()}}function E(){var Q=5;return Math.abs(O.second.x-O.first.x)>=Q&&Math.abs(O.second.y-O.first.y)>=Q}J.clearSelection=P;J.setSelection=N;J.getSelection=F;J.hooks.bindEvents.push(function(R,Q){var S=R.getOptions();if(S.selection.mode!=null){Q.mousemove(D)}if(S.selection.mode!=null){Q.mousedown(M)}});J.hooks.drawOverlay.push(function(T,Y){if(O.show&&E()){var R=T.getPlotOffset();var Q=T.getOptions();Y.save();Y.translate(R.left,R.top);var U=A.color.parse(Q.selection.color);Y.strokeStyle=U.scale("a",0.8).toString();Y.lineWidth=1;Y.lineJoin="round";Y.fillStyle=U.scale("a",0.4).toString();var W=Math.min(O.first.x,O.second.x),V=Math.min(O.first.y,O.second.y),X=Math.abs(O.second.x-O.first.x),S=Math.abs(O.second.y-O.first.y);Y.fillRect(W,V,X,S);Y.strokeRect(W,V,X,S);Y.restore()}})}A.plot.plugins.push({init:B,options:{selection:{mode:null,color:"#e8cfac"}},name:"selection",version:"1.0"})})(jQuery);
|
|
@ -0,0 +1,152 @@
|
||||||
|
/*
|
||||||
|
Flot plugin for stacking data sets, i.e. putting them on top of each
|
||||||
|
other, for accumulative graphs. Note that the plugin assumes the data
|
||||||
|
is sorted on x. Also note that stacking a mix of positive and negative
|
||||||
|
values in most instances doesn't make sense (so it looks weird).
|
||||||
|
|
||||||
|
Two or more series are stacked when their "stack" attribute is set to
|
||||||
|
the same key (which can be any number or string or just "true"). To
|
||||||
|
specify the default stack, you can set
|
||||||
|
|
||||||
|
series: {
|
||||||
|
stack: null or true or key (number/string)
|
||||||
|
}
|
||||||
|
|
||||||
|
or specify it for a specific series
|
||||||
|
|
||||||
|
$.plot($("#placeholder"), [{ data: [ ... ], stack: true ])
|
||||||
|
|
||||||
|
The stacking order is determined by the order of the data series in
|
||||||
|
the array (later series end up on top of the previous).
|
||||||
|
|
||||||
|
Internally, the plugin modifies the datapoints in each series, adding
|
||||||
|
an offset to the y value. For line series, extra data points are
|
||||||
|
inserted through interpolation. For bar charts, the second y value is
|
||||||
|
also adjusted.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
var options = {
|
||||||
|
series: { stack: null } // or number/string
|
||||||
|
};
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
function findMatchingSeries(s, allseries) {
|
||||||
|
var res = null
|
||||||
|
for (var i = 0; i < allseries.length; ++i) {
|
||||||
|
if (s == allseries[i])
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (allseries[i].stack == s.stack)
|
||||||
|
res = allseries[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stackData(plot, s, datapoints) {
|
||||||
|
if (s.stack == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var other = findMatchingSeries(s, plot.getData());
|
||||||
|
if (!other)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var ps = datapoints.pointsize,
|
||||||
|
points = datapoints.points,
|
||||||
|
otherps = other.datapoints.pointsize,
|
||||||
|
otherpoints = other.datapoints.points,
|
||||||
|
newpoints = [],
|
||||||
|
px, py, intery, qx, qy, bottom,
|
||||||
|
withlines = s.lines.show, withbars = s.bars.show,
|
||||||
|
withsteps = withlines && s.lines.steps,
|
||||||
|
i = 0, j = 0, l;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (i >= points.length)
|
||||||
|
break;
|
||||||
|
|
||||||
|
l = newpoints.length;
|
||||||
|
|
||||||
|
if (j >= otherpoints.length
|
||||||
|
|| otherpoints[j] == null
|
||||||
|
|| points[i] == null) {
|
||||||
|
// degenerate cases
|
||||||
|
for (m = 0; m < ps; ++m)
|
||||||
|
newpoints.push(points[i + m]);
|
||||||
|
i += ps;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// cases where we actually got two points
|
||||||
|
px = points[i];
|
||||||
|
py = points[i + 1];
|
||||||
|
qx = otherpoints[j];
|
||||||
|
qy = otherpoints[j + 1];
|
||||||
|
bottom = 0;
|
||||||
|
|
||||||
|
if (px == qx) {
|
||||||
|
for (m = 0; m < ps; ++m)
|
||||||
|
newpoints.push(points[i + m]);
|
||||||
|
|
||||||
|
newpoints[l + 1] += qy;
|
||||||
|
bottom = qy;
|
||||||
|
|
||||||
|
i += ps;
|
||||||
|
j += otherps;
|
||||||
|
}
|
||||||
|
else if (px > qx) {
|
||||||
|
// we got past point below, might need to
|
||||||
|
// insert interpolated extra point
|
||||||
|
if (withlines && i > 0 && points[i - ps] != null) {
|
||||||
|
intery = py + (points[i - ps + 1] - py) * (qx - px) / (points[i - ps] - px);
|
||||||
|
newpoints.push(qx);
|
||||||
|
newpoints.push(intery + qy)
|
||||||
|
for (m = 2; m < ps; ++m)
|
||||||
|
newpoints.push(points[i + m]);
|
||||||
|
bottom = qy;
|
||||||
|
}
|
||||||
|
|
||||||
|
j += otherps;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (m = 0; m < ps; ++m)
|
||||||
|
newpoints.push(points[i + m]);
|
||||||
|
|
||||||
|
// we might be able to interpolate a point below,
|
||||||
|
// this can give us a better y
|
||||||
|
if (withlines && j > 0 && otherpoints[j - ps] != null)
|
||||||
|
bottom = qy + (otherpoints[j - ps + 1] - qy) * (px - qx) / (otherpoints[j - ps] - qx);
|
||||||
|
|
||||||
|
newpoints[l + 1] += bottom;
|
||||||
|
|
||||||
|
i += ps;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (l != newpoints.length && withbars)
|
||||||
|
newpoints[l + 2] += bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maintain the line steps invariant
|
||||||
|
if (withsteps && l != newpoints.length && l > 0
|
||||||
|
&& newpoints[l] != null
|
||||||
|
&& newpoints[l] != newpoints[l - ps]
|
||||||
|
&& newpoints[l + 1] != newpoints[l - ps + 1]) {
|
||||||
|
for (m = 0; m < ps; ++m)
|
||||||
|
newpoints[l + ps + m] = newpoints[l + m];
|
||||||
|
newpoints[l + 1] = newpoints[l - ps + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
datapoints.points = newpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.hooks.processDatapoints.push(stackData);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'stack',
|
||||||
|
version: '1.0'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
1
browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.stack.min.js
поставляемый
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(function(B){var A={series:{stack:null}};function C(F){function D(J,I){var H=null;for(var G=0;G<I.length;++G){if(J==I[G]){break}if(I[G].stack==J.stack){H=I[G]}}return H}function E(W,P,G){if(P.stack==null){return }var L=D(P,W.getData());if(!L){return }var T=G.pointsize,Y=G.points,H=L.datapoints.pointsize,S=L.datapoints.points,N=[],R,Q,I,a,Z,M,O=P.lines.show,K=P.bars.show,J=O&&P.lines.steps,X=0,V=0,U;while(true){if(X>=Y.length){break}U=N.length;if(V>=S.length||S[V]==null||Y[X]==null){for(m=0;m<T;++m){N.push(Y[X+m])}X+=T}else{R=Y[X];Q=Y[X+1];a=S[V];Z=S[V+1];M=0;if(R==a){for(m=0;m<T;++m){N.push(Y[X+m])}N[U+1]+=Z;M=Z;X+=T;V+=H}else{if(R>a){if(O&&X>0&&Y[X-T]!=null){I=Q+(Y[X-T+1]-Q)*(a-R)/(Y[X-T]-R);N.push(a);N.push(I+Z);for(m=2;m<T;++m){N.push(Y[X+m])}M=Z}V+=H}else{for(m=0;m<T;++m){N.push(Y[X+m])}if(O&&V>0&&S[V-T]!=null){M=Z+(S[V-T+1]-Z)*(R-a)/(S[V-T]-a)}N[U+1]+=M;X+=T}}if(U!=N.length&&K){N[U+2]+=M}}if(J&&U!=N.length&&U>0&&N[U]!=null&&N[U]!=N[U-T]&&N[U+1]!=N[U-T+1]){for(m=0;m<T;++m){N[U+T+m]=N[U+m]}N[U+1]=N[U-T+1]}}G.points=N}F.hooks.processDatapoints.push(E)}B.plot.plugins.push({init:C,options:A,name:"stack",version:"1.0"})})(jQuery);
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
Flot plugin for thresholding data. Controlled through the option
|
||||||
|
"threshold" in either the global series options
|
||||||
|
|
||||||
|
series: {
|
||||||
|
threshold: {
|
||||||
|
below: number
|
||||||
|
color: colorspec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
or in a specific series
|
||||||
|
|
||||||
|
$.plot($("#placeholder"), [{ data: [ ... ], threshold: { ... }}])
|
||||||
|
|
||||||
|
The data points below "below" are drawn with the specified color. This
|
||||||
|
makes it easy to mark points below 0, e.g. for budget data.
|
||||||
|
|
||||||
|
Internally, the plugin works by splitting the data into two series,
|
||||||
|
above and below the threshold. The extra series below the threshold
|
||||||
|
will have its label cleared and the special "originSeries" attribute
|
||||||
|
set to the original series. You may need to check for this in hover
|
||||||
|
events.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
var options = {
|
||||||
|
series: { threshold: null } // or { below: number, color: color spec}
|
||||||
|
};
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
function thresholdData(plot, s, datapoints) {
|
||||||
|
if (!s.threshold)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var ps = datapoints.pointsize, i, x, y, p, prevp,
|
||||||
|
thresholded = $.extend({}, s); // note: shallow copy
|
||||||
|
|
||||||
|
thresholded.datapoints = { points: [], pointsize: ps };
|
||||||
|
thresholded.label = null;
|
||||||
|
thresholded.color = s.threshold.color;
|
||||||
|
thresholded.threshold = null;
|
||||||
|
thresholded.originSeries = s;
|
||||||
|
thresholded.data = [];
|
||||||
|
|
||||||
|
var below = s.threshold.below,
|
||||||
|
origpoints = datapoints.points,
|
||||||
|
addCrossingPoints = s.lines.show;
|
||||||
|
|
||||||
|
threspoints = [];
|
||||||
|
newpoints = [];
|
||||||
|
|
||||||
|
for (i = 0; i < origpoints.length; i += ps) {
|
||||||
|
x = origpoints[i]
|
||||||
|
y = origpoints[i + 1];
|
||||||
|
|
||||||
|
prevp = p;
|
||||||
|
if (y < below)
|
||||||
|
p = threspoints;
|
||||||
|
else
|
||||||
|
p = newpoints;
|
||||||
|
|
||||||
|
if (addCrossingPoints && prevp != p && x != null
|
||||||
|
&& i > 0 && origpoints[i - ps] != null) {
|
||||||
|
var interx = (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]) * (below - y) + x;
|
||||||
|
prevp.push(interx);
|
||||||
|
prevp.push(below);
|
||||||
|
for (m = 2; m < ps; ++m)
|
||||||
|
prevp.push(origpoints[i + m]);
|
||||||
|
|
||||||
|
p.push(null); // start new segment
|
||||||
|
p.push(null);
|
||||||
|
for (m = 2; m < ps; ++m)
|
||||||
|
p.push(origpoints[i + m]);
|
||||||
|
p.push(interx);
|
||||||
|
p.push(below);
|
||||||
|
for (m = 2; m < ps; ++m)
|
||||||
|
p.push(origpoints[i + m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.push(x);
|
||||||
|
p.push(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
datapoints.points = newpoints;
|
||||||
|
thresholded.datapoints.points = threspoints;
|
||||||
|
|
||||||
|
if (thresholded.datapoints.points.length > 0)
|
||||||
|
plot.getData().push(thresholded);
|
||||||
|
|
||||||
|
// FIXME: there are probably some edge cases left in bars
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.hooks.processDatapoints.push(thresholdData);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'threshold',
|
||||||
|
version: '1.0'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
1
browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.flot.threshold.min.js
поставляемый
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(function(B){var A={series:{threshold:null}};function C(D){function E(L,S,M){if(!S.threshold){return }var F=M.pointsize,I,O,N,G,K,H=B.extend({},S);H.datapoints={points:[],pointsize:F};H.label=null;H.color=S.threshold.color;H.threshold=null;H.originSeries=S;H.data=[];var P=S.threshold.below,Q=M.points,R=S.lines.show;threspoints=[];newpoints=[];for(I=0;I<Q.length;I+=F){O=Q[I];N=Q[I+1];K=G;if(N<P){G=threspoints}else{G=newpoints}if(R&&K!=G&&O!=null&&I>0&&Q[I-F]!=null){var J=(O-Q[I-F])/(N-Q[I-F+1])*(P-N)+O;K.push(J);K.push(P);for(m=2;m<F;++m){K.push(Q[I+m])}G.push(null);G.push(null);for(m=2;m<F;++m){G.push(Q[I+m])}G.push(J);G.push(P);for(m=2;m<F;++m){G.push(Q[I+m])}}G.push(O);G.push(N)}M.points=newpoints;H.datapoints.points=threspoints;if(H.datapoints.points.length>0){L.getData().push(H)}}D.hooks.processDatapoints.push(E)}B.plot.plugins.push({init:C,options:A,name:"threshold",version:"1.0"})})(jQuery);
|
4376
browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.js
поставляемый
Normal file
19
browser/app/profile/extensions/testpilot@labs.mozilla.com/content/flot/jquery.min.js
поставляемый
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Test Pilot.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
* Raymond Lee <raymond@appcoast.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
const Cu = Components.utils;
|
||||||
|
|
||||||
|
Cu.import("resource://testpilot/modules/setup.js");
|
||||||
|
|
||||||
|
function showdbcontents() {
|
||||||
|
// experimentId is passed in through window.openDialog. Access it like so:
|
||||||
|
var experimentId = window.arguments[0];
|
||||||
|
var experiment = TestPilotSetup.getTaskById(experimentId);
|
||||||
|
var dataStore = experiment.dataStore;
|
||||||
|
var experimentTitle = experiment.title;
|
||||||
|
var listbox = document.getElementById("raw-data-listbox");
|
||||||
|
var columnNames = dataStore.getHumanReadableColumnNames();
|
||||||
|
var propertyNames = dataStore.getPropertyNames();
|
||||||
|
|
||||||
|
// Set title of dialog box to match title of experiment:
|
||||||
|
var dialog = document.getElementById("raw-data-dialog");
|
||||||
|
var dialogTitle = dialog.getAttribute("title");
|
||||||
|
dialogTitle = dialogTitle + ": " + experimentTitle;
|
||||||
|
dialog.setAttribute("title", dialogTitle);
|
||||||
|
|
||||||
|
var i,j;
|
||||||
|
// Create the listcols and listheaders in the xul listbox to match the human
|
||||||
|
// readable column names provided:
|
||||||
|
var listcols = document.getElementById("raw-data-listcols");
|
||||||
|
var listhead = document.getElementById("raw-data-listhead");
|
||||||
|
for (j = 0; j < columnNames.length; j++) {
|
||||||
|
listcols.appendChild(document.createElement("listcol"));
|
||||||
|
var newHeader = document.createElement("listheader");
|
||||||
|
newHeader.setAttribute("label", columnNames[j]);
|
||||||
|
listhead.appendChild(newHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
dataStore.getAllDataAsJSON(true, function(rawData) {
|
||||||
|
// Convert each object in the JSON into a row of the listbox.
|
||||||
|
for (i = 0; i < rawData.length; i++) {
|
||||||
|
var row = document.createElement("listitem");
|
||||||
|
for (j = 0; j < columnNames.length; j++) {
|
||||||
|
var cell = document.createElement("listcell");
|
||||||
|
var value = rawData[i][propertyNames[j]];
|
||||||
|
cell.setAttribute("label", value);
|
||||||
|
row.appendChild(cell);
|
||||||
|
}
|
||||||
|
listbox.appendChild(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK button for dialog box.
|
||||||
|
function onAccept() {
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
|
||||||
|
|
||||||
|
<!DOCTYPE dialog [
|
||||||
|
<!ENTITY % testpilotDTD SYSTEM "chrome://testpilot/locale/main.dtd">
|
||||||
|
%testpilotDTD;
|
||||||
|
]>
|
||||||
|
|
||||||
|
<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||||
|
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||||
|
id="raw-data-dialog" title="&testpilot.rawDataWindow.title;"
|
||||||
|
buttons="accept"
|
||||||
|
onload="showdbcontents();"
|
||||||
|
ondialogaccept="return onAccept();">
|
||||||
|
|
||||||
|
<script src="chrome://testpilot/content/raw-data-dialog.js"/>
|
||||||
|
|
||||||
|
<listbox id="raw-data-listbox" rows="20">
|
||||||
|
<listcols id="raw-data-listcols"></listcols>
|
||||||
|
<listhead id="raw-data-listhead"></listhead>
|
||||||
|
</listbox>
|
||||||
|
|
||||||
|
</dialog>
|
|
@ -0,0 +1,258 @@
|
||||||
|
html {
|
||||||
|
padding-bottom: 0px;
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Lucida Grande', 'Lucida Sans Unicode', Lucida, Arial, Helvetica, sans-serif;
|
||||||
|
font-family: 'DroidSans';
|
||||||
|
background: #fff url('chrome://testpilot/skin/bg.jpg') repeat-x top center;
|
||||||
|
padding: 0px;
|
||||||
|
color:#787878;
|
||||||
|
font-size:12px;
|
||||||
|
line-height: 18px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {font-family: 'DroidSans'; color: #3a3a3a; font-size: 28px; font-weight: normal; letter-spacing: -1px}
|
||||||
|
h2 {font-family: 'DroidSans'; color: #54717b; font-size: 24px; line-height: 22px;font-weight: normal; letter-spacing: -1px}
|
||||||
|
|
||||||
|
p, ul {font-size: 12px;}
|
||||||
|
|
||||||
|
a:link, a:hover, a:active, a:focus, a:visited {color: #9f423b; text-decoration: none;}
|
||||||
|
a:hover {color: #9f423b; text-decoration: none;}
|
||||||
|
|
||||||
|
.bold {font-family: 'DroidSans-Bold'; color:#3a3a3a;}
|
||||||
|
.address {margin-left: 20px;}
|
||||||
|
.inactive {color:#ccc;}
|
||||||
|
|
||||||
|
li {list-style-type: circle;}
|
||||||
|
|
||||||
|
.spacer {height: 75px;}
|
||||||
|
|
||||||
|
|
||||||
|
@font-face{
|
||||||
|
font-family: 'DroidSans';
|
||||||
|
src: url('chrome://testpilot/skin/fonts/DroidSans.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face{
|
||||||
|
font-family: 'DroidSans-Bold';
|
||||||
|
src: url('chrome://testpilot/skin/fonts/DroidSans-Bold.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
margin: 0px auto;
|
||||||
|
width: 950px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#logo {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#contentWelcome {
|
||||||
|
margin-top: 120px;
|
||||||
|
padding: 8px 24px 24px 24px;
|
||||||
|
font-family: 'DroidSans';
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
margin-top: 60px;
|
||||||
|
padding: 8px 24px 24px 24px;
|
||||||
|
font-family: 'DroidSans';
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#download {
|
||||||
|
width: 300px;
|
||||||
|
margin-left: 410px;
|
||||||
|
margin-top: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadButton {
|
||||||
|
margin-bottom: -10px;
|
||||||
|
margin-top: -2px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadH1 {font-family: 'DroidSans-bold'; color: #3a3a3a; font-size: 24px; font-weight: normal; letter-spacing: -1px; margin-right: 8px;}
|
||||||
|
|
||||||
|
.downloadH2 {font-family: 'DroidSans'; color: #3a3a3a; font-size: 22px; font-weight: normal; letter-spacing: -1px; line-height: 28px; margin-left: 46px;}
|
||||||
|
|
||||||
|
#intro {
|
||||||
|
float: left;
|
||||||
|
width: 500px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#links {
|
||||||
|
float: right;
|
||||||
|
padding: 24px 0px;
|
||||||
|
margin-right: 0px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
font-family: 'DroidSans';
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
-moz-border-radius: 0.5em;
|
||||||
|
-webkit-border-radius: 0.5em;
|
||||||
|
-moz-box-shadow:
|
||||||
|
inset rgba(0, 0, 0, 0.2) 0 1px 1px,
|
||||||
|
inset rgba(255, 255, 255, 1) 0 3px 1px,
|
||||||
|
inset rgba(255, 255, 255, 0.3) 0 16px 0px,
|
||||||
|
inset rgba(0, 0, 0, 0.2) 0 -1px 1px,
|
||||||
|
inset rgba(0, 0, 0, 0.1) 0 -2px 1px,
|
||||||
|
rgba(255, 255, 255, 1) 0 1px,
|
||||||
|
rgba(133, 153, 166, 0.3) 0px 1px 12px;
|
||||||
|
background-color: #e7eaec;
|
||||||
|
//display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home_button {
|
||||||
|
font-family: 'DroidSans';
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 240px;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
-moz-border-radius: 0.5em;
|
||||||
|
-webkit-border-radius: 0.5em;
|
||||||
|
-moz-box-shadow:
|
||||||
|
inset rgba(0, 0, 0, 0.2) 0 1px 1px,
|
||||||
|
inset rgba(255, 255, 255, 1) 0 3px 1px,
|
||||||
|
inset rgba(255, 255, 255, 0.3) 0 16px 0px,
|
||||||
|
inset rgba(0, 0, 0, 0.2) 0 -1px 1px,
|
||||||
|
inset rgba(0, 0, 0, 0.1) 0 -2px 1px,
|
||||||
|
rgba(255, 255, 255, 1) 0 1px,
|
||||||
|
rgba(133, 153, 166, 0.3) 0px 1px 12px;
|
||||||
|
background-color: #e7eaec;
|
||||||
|
//display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.callout {
|
||||||
|
font-family: 'DroidSans';
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 8px 24px;
|
||||||
|
margin: 24px auto;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
-moz-border-radius: 0.5em;
|
||||||
|
-webkit-border-radius: 0.5em;
|
||||||
|
background: rgba(220, 240, 247, 0.8) url('chrome://testpilot/skin/callout.png') no-repeat top center;
|
||||||
|
-moz-box-shadow:
|
||||||
|
inset rgba(185, 221, 234, 0.2) 0 -10px 12px,
|
||||||
|
inset rgba(185, 221, 234, 1) 0 0px 1px,
|
||||||
|
inset rgba(255, 255, 255, 0.2) 0 10px 12px;
|
||||||
|
//display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home_callout {
|
||||||
|
font-family: 'DroidSans';
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 240px;
|
||||||
|
padding: 8px 24px;
|
||||||
|
margin: 8px auto;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
-moz-border-radius: 0.5em;
|
||||||
|
-webkit-border-radius: 0.5em;
|
||||||
|
background: rgba(220, 240, 247, 0.8) url('chrome://testpilot/skin/callout.png') no-repeat top center;
|
||||||
|
-moz-box-shadow:
|
||||||
|
inset rgba(185, 221, 234, 0.2) 0 -10px 12px,
|
||||||
|
inset rgba(185, 221, 234, 1) 0 0px 1px,
|
||||||
|
inset rgba(255, 255, 255, 0.2) 0 10px 12px;
|
||||||
|
//display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.homeIcon {
|
||||||
|
margin-top: -32px;
|
||||||
|
margin-bottom: -32px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
clear: both;
|
||||||
|
padding: 24px;
|
||||||
|
font-size: 9px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.mozLogo {
|
||||||
|
margin-bottom: -10px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- MENU -------
|
||||||
|
|
||||||
|
#menu {
|
||||||
|
margin: 20px auto;
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 4px 40px;
|
||||||
|
width: 800px;
|
||||||
|
text-align: left;
|
||||||
|
-moz-border-radius: 0.25em;
|
||||||
|
-webkit-border-radius: 0.25em;
|
||||||
|
border-top: 1px solid #adb6ba;
|
||||||
|
border-left: 1px solid #adb6ba;
|
||||||
|
border-right: 1px solid #adb6ba;
|
||||||
|
border-bottom: 3px solid #adb6ba;
|
||||||
|
-moz-border-bottom-colors:#adb6ba #e7eaec #e7eaec;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
.menuItem {
|
||||||
|
margin-right: 30px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-shadow: 1px 1px 1px rgba(173, 182, 186, 0.6);
|
||||||
|
padding: 9px 8px 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuOn {
|
||||||
|
margin-right: 30px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-shadow: 1px 1px 1px rgba(173, 182, 186, 1);
|
||||||
|
background-color: rgba(173, 182, 186, 0.3);
|
||||||
|
-moz-box-shadow:
|
||||||
|
inset rgba(0, 0, 0, 0.2) 0 -10px 12px;
|
||||||
|
padding: 9px 8px 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuItem a {color: #9f423b; text-decoration: none;}
|
||||||
|
.menuItem a:hover {color: #9f423b; text-decoration: none; border-bottom: 1px dotted #9f423b;}
|
||||||
|
|
||||||
|
|
||||||
|
.menuOn a {color: #666666; text-decoration: none;}
|
||||||
|
.menuOn a:hover {color: #666666; text-decoration: none;}
|
||||||
|
|
||||||
|
#get-started {
|
||||||
|
float: left;
|
||||||
|
width: 45%;
|
||||||
|
margin: 20px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#icon-explanation {
|
||||||
|
float: right;
|
||||||
|
width: 45%;
|
||||||
|
margin: 20px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-link {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #9f423b;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.embiggened {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||||
|
<title id="page-title"></title>
|
||||||
|
<link rel="stylesheet" type="text/css" media="all"
|
||||||
|
href="chrome://testpilot/skin/css/screen-standalone.css" />
|
||||||
|
<script src="experiment-page.js" type="application/javascript;version=1.8">
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body onload="onQuitPageLoad();">
|
||||||
|
<div id="container">
|
||||||
|
<div id="content-standalone">
|
||||||
|
<div id="intro">
|
||||||
|
<h2 id="about-quit-title"></h2>
|
||||||
|
<p id="optional-message"></p>
|
||||||
|
<p><span id="reason-text"></span>
|
||||||
|
<textarea rows="6" cols="64" id="reason-for-quit"></textarea></p>
|
||||||
|
<p id="recur-options" style="display:none"></p><br/>
|
||||||
|
<div id="recur-checkbox-container" style="display:none">
|
||||||
|
<input type="checkbox" id="opt-out-forever">
|
||||||
|
<span id="quit-forever-text"></span>
|
||||||
|
</div>
|
||||||
|
<div class="home_callout_continue" style="text-align:center">
|
||||||
|
<a id="quit-study-link" onclick="quitExperiment();"></a></div>
|
||||||
|
</div>
|
||||||
|
<div id="links">
|
||||||
|
<p class="spacer"></p>
|
||||||
|
<div class="home_callout">
|
||||||
|
<img class="homeIcon"
|
||||||
|
src="chrome://testpilot/skin/images/home_comments.png">
|
||||||
|
<a id="comments-and-discussions-link"
|
||||||
|
onclick="
|
||||||
|
openLink('http://groups.google.com/group/mozilla-labs-testpilot');">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="home_callout">
|
||||||
|
<img class="homeIcon"
|
||||||
|
src="chrome://testpilot/skin/images/home_results.png">
|
||||||
|
<a id="propose-test-link"
|
||||||
|
onclick="openLink('https://wiki.mozilla.org/Labs/Test_Pilot');"></a>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="home_callout">
|
||||||
|
<img class="homeIcon"
|
||||||
|
src="chrome://testpilot/skin/images/home_twitter.png">
|
||||||
|
<a id="testpilot-twitter-link"
|
||||||
|
onclick="openLink('http://www.twitter.com/MozTestPilot');"></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||||
|
<title id="page-title"></title>
|
||||||
|
<link rel="stylesheet" type="text/css" media="all"
|
||||||
|
href="chrome://testpilot/skin/css/screen-standalone.css">
|
||||||
|
<script src="experiment-page.js"
|
||||||
|
type="application/javascript;version=1.8"></script>
|
||||||
|
<script src="flot/jquery.js" language="javascript"
|
||||||
|
type="text/javascript"></script>
|
||||||
|
<script src="flot/jquery.flot.js" language="javascript"
|
||||||
|
type="text/javascript"></script>
|
||||||
|
</head>
|
||||||
|
<body onload="onStatusPageLoad();">
|
||||||
|
<div id="container">
|
||||||
|
<div id="content-standalone">
|
||||||
|
<div id="intro">
|
||||||
|
<span id="recur-pref"></span>
|
||||||
|
<span id="recur-controls"></span>
|
||||||
|
<div id="experiment-specific-text"></div>
|
||||||
|
</div>
|
||||||
|
<div id="links">
|
||||||
|
<p class="spacer">
|
||||||
|
<div id="data-privacy-text" hidden="true"></div>
|
||||||
|
<div class="home_callout">
|
||||||
|
<img class="homeIcon"
|
||||||
|
src="chrome://testpilot/skin/images/home_comments.png">
|
||||||
|
<a id="comments-and-discussions-link"
|
||||||
|
onclick="
|
||||||
|
openLink('http://groups.google.com/group/mozilla-labs-testpilot');">
|
||||||
|
</a>
|
||||||
|
</div><br>
|
||||||
|
<div class="home_callout">
|
||||||
|
<img class="homeIcon"
|
||||||
|
src="chrome://testpilot/skin/images/home_results.png">
|
||||||
|
<a id="propose-test-link"
|
||||||
|
onclick="openLink('https://wiki.mozilla.org/Labs/Test_Pilot');">
|
||||||
|
</a>
|
||||||
|
</div><br>
|
||||||
|
<div class="home_callout">
|
||||||
|
<img class="homeIcon"
|
||||||
|
src="chrome://testpilot/skin/images/home_twitter.png">
|
||||||
|
<a id="testpilot-twitter-link"
|
||||||
|
onclick="openLink('http://www.twitter.com/MozTestPilot');">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,326 @@
|
||||||
|
const MULTIPLE_CHOICE = 0;
|
||||||
|
const CHECK_BOXES_WITH_FREE_ENTRY = 1;
|
||||||
|
const SCALE = 2;
|
||||||
|
const FREE_ENTRY = 3;
|
||||||
|
const CHECK_BOXES = 4;
|
||||||
|
const MULTIPLE_CHOICE_WITH_FREE_ENTRY = 5;
|
||||||
|
|
||||||
|
var stringBundle;
|
||||||
|
|
||||||
|
function onBuiltinSurveyLoad() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
Components.utils.import("resource://testpilot/modules/tasks.js");
|
||||||
|
setStrings();
|
||||||
|
let eid = getUrlParam("eid");
|
||||||
|
let task = TestPilotSetup.getTaskById(eid);
|
||||||
|
let contentDiv = document.getElementById("survey-contents");
|
||||||
|
let explanation = document.getElementById("survey-explanation");
|
||||||
|
if (!task) {
|
||||||
|
// Tasks haven't all loaded yet. Try again in a few seconds.
|
||||||
|
contentDiv.innerHTML =
|
||||||
|
stringBundle.GetStringFromName("testpilot.surveyPage.loading");
|
||||||
|
window.setTimeout(function() { onBuiltinSurveyLoad(); }, 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = document.getElementById("survey-title");
|
||||||
|
title.innerHTML = task.title;
|
||||||
|
|
||||||
|
let submitButton = document.getElementById("survey-submit");
|
||||||
|
if (task.relatedStudyId) {
|
||||||
|
submitButton.innerHTML =
|
||||||
|
stringBundle.GetStringFromName("testpilot.surveyPage.submitAnswers");
|
||||||
|
} else {
|
||||||
|
submitButton.innerHTML =
|
||||||
|
stringBundle.GetStringFromName("testpilot.surveyPage.saveAnswers");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.status == TaskConstants.STATUS_SUBMITTED) {
|
||||||
|
contentDiv.innerHTML =
|
||||||
|
"<p>" +
|
||||||
|
stringBundle.GetStringFromName(
|
||||||
|
"testpilot.surveyPage.thankYouForFinishingSurvey") + "</p><p>" +
|
||||||
|
stringBundle.GetStringFromName(
|
||||||
|
"testpilot.surveyPage.reviewOrChangeYourAnswers") + "</p>";
|
||||||
|
explanation.innerHTML = "";
|
||||||
|
submitButton.setAttribute("style", "display:none");
|
||||||
|
let changeButton = document.getElementById("change-answers");
|
||||||
|
changeButton.setAttribute("style", "");
|
||||||
|
} else {
|
||||||
|
contentDiv.innerHTML = "";
|
||||||
|
if (task.surveyExplanation) {
|
||||||
|
explanation.innerHTML = task.surveyExplanation;
|
||||||
|
} else {
|
||||||
|
explanation.innerHTML = "";
|
||||||
|
}
|
||||||
|
drawSurveyForm(task, contentDiv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSurveyForm(task, contentDiv) {
|
||||||
|
let oldAnswers = task.oldAnswers;
|
||||||
|
let surveyQuestions = task.surveyQuestions;
|
||||||
|
|
||||||
|
let submitButton = document.getElementById("survey-submit");
|
||||||
|
submitButton.setAttribute("style", "");
|
||||||
|
let changeButton = document.getElementById("change-answers");
|
||||||
|
changeButton.setAttribute("style", "display:none");
|
||||||
|
// Loop through questions and render html form input elements for each
|
||||||
|
// one.
|
||||||
|
for (let i = 0; i < surveyQuestions.length; i++) {
|
||||||
|
let question = surveyQuestions[i].question;
|
||||||
|
let explanation = surveyQuestions[i].explanation;
|
||||||
|
let elem;
|
||||||
|
|
||||||
|
elem = document.createElement("h3");
|
||||||
|
elem.innerHTML = (i+1) + ". " + question;
|
||||||
|
contentDiv.appendChild(elem);
|
||||||
|
if (explanation) {
|
||||||
|
elem = document.createElement("p");
|
||||||
|
elem.setAttribute("class", "survey-question-explanation");
|
||||||
|
elem.innerHTML = explanation;
|
||||||
|
contentDiv.appendChild(elem);
|
||||||
|
}
|
||||||
|
// If you've done this survey before, preset all inputs using old answers
|
||||||
|
let choices = surveyQuestions[i].choices;
|
||||||
|
switch (surveyQuestions[i].type) {
|
||||||
|
case MULTIPLE_CHOICE:
|
||||||
|
for (let j = 0; j < choices.length; j++) {
|
||||||
|
let newRadio = document.createElement("input");
|
||||||
|
newRadio.setAttribute("type", "radio");
|
||||||
|
newRadio.setAttribute("name", "answer_to_" + i);
|
||||||
|
newRadio.setAttribute("value", j);
|
||||||
|
if (oldAnswers && oldAnswers[i] == String(j)) {
|
||||||
|
newRadio.setAttribute("checked", "true");
|
||||||
|
}
|
||||||
|
let label = document.createElement("span");
|
||||||
|
label.innerHTML = choices[j];
|
||||||
|
contentDiv.appendChild(newRadio);
|
||||||
|
contentDiv.appendChild(label);
|
||||||
|
contentDiv.appendChild(document.createElement("br"));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CHECK_BOXES:
|
||||||
|
case CHECK_BOXES_WITH_FREE_ENTRY:
|
||||||
|
let checkboxName = "answer_to_" + i;
|
||||||
|
// Check boxes:
|
||||||
|
for (let j = 0; j < choices.length; j++) {
|
||||||
|
let newCheck = document.createElement("input");
|
||||||
|
newCheck.setAttribute("type", "checkbox");
|
||||||
|
newCheck.setAttribute("name", checkboxName);
|
||||||
|
newCheck.setAttribute("value", j);
|
||||||
|
if (oldAnswers && oldAnswers[i]) {
|
||||||
|
for each (let an in oldAnswers[i]) {
|
||||||
|
if (an == String(j)) {
|
||||||
|
newCheck.setAttribute("checked", "true");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let label = document.createElement("span");
|
||||||
|
label.innerHTML = choices[j];
|
||||||
|
contentDiv.appendChild(newCheck);
|
||||||
|
contentDiv.appendChild(label);
|
||||||
|
contentDiv.appendChild(document.createElement("br"));
|
||||||
|
}
|
||||||
|
// Text area:
|
||||||
|
if (surveyQuestions[i].type == CHECK_BOXES_WITH_FREE_ENTRY &&
|
||||||
|
surveyQuestions[i].free_entry) {
|
||||||
|
let freeformId = "freeform_" + i;
|
||||||
|
let newCheck = document.createElement("input");
|
||||||
|
newCheck.setAttribute("type", "checkbox");
|
||||||
|
newCheck.setAttribute("name", checkboxName);
|
||||||
|
newCheck.setAttribute("value", freeformId);
|
||||||
|
newCheck.addEventListener(
|
||||||
|
"click", function(event) {
|
||||||
|
if (!event.target.checked) {
|
||||||
|
document.getElementById(freeformId).value = "";
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
let label = document.createElement("span");
|
||||||
|
label.innerHTML = surveyQuestions[i].free_entry + " : ";
|
||||||
|
let inputBox = document.createElement("textarea");
|
||||||
|
inputBox.setAttribute("id", freeformId);
|
||||||
|
inputBox.addEventListener(
|
||||||
|
"keypress", function() {
|
||||||
|
let elements = document.getElementsByName(checkboxName);
|
||||||
|
for (let j = (elements.length - 1); j >= 0; j--) {
|
||||||
|
if (elements[j].value == freeformId) {
|
||||||
|
elements[j].checked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
if (oldAnswers && oldAnswers[i]) {
|
||||||
|
for each (let an in oldAnswers[i]) {
|
||||||
|
if (isNaN(parseInt(an))) {
|
||||||
|
newCheck.setAttribute("checked", "true");
|
||||||
|
inputBox.value = an;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentDiv.appendChild(newCheck);
|
||||||
|
contentDiv.appendChild(label);
|
||||||
|
contentDiv.appendChild(inputBox);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SCALE:
|
||||||
|
let label = document.createElement("span");
|
||||||
|
label.innerHTML = surveyQuestions[i].min_label;
|
||||||
|
contentDiv.appendChild(label);
|
||||||
|
for (let j = surveyQuestions[i].scale_minimum;
|
||||||
|
j <= surveyQuestions[i].scale_maximum;
|
||||||
|
j++) {
|
||||||
|
let newRadio = document.createElement("input");
|
||||||
|
newRadio.setAttribute("type", "radio");
|
||||||
|
newRadio.setAttribute("name", "answer_to_" + i);
|
||||||
|
newRadio.setAttribute("value", j);
|
||||||
|
if (oldAnswers && oldAnswers[i] == String(j)) {
|
||||||
|
newRadio.setAttribute("checked", "true");
|
||||||
|
}
|
||||||
|
contentDiv.appendChild(newRadio);
|
||||||
|
}
|
||||||
|
label = document.createElement("span");
|
||||||
|
label.innerHTML = surveyQuestions[i].max_label;
|
||||||
|
contentDiv.appendChild(label);
|
||||||
|
break;
|
||||||
|
case FREE_ENTRY:
|
||||||
|
let inputBox = document.createElement("textarea");
|
||||||
|
inputBox.setAttribute("id", "freeform_" + i);
|
||||||
|
|
||||||
|
if (oldAnswers && oldAnswers[i] && (oldAnswers[i].length > 0)) {
|
||||||
|
inputBox.value = oldAnswers[i];
|
||||||
|
}
|
||||||
|
contentDiv.appendChild(inputBox);
|
||||||
|
break;
|
||||||
|
case MULTIPLE_CHOICE_WITH_FREE_ENTRY:
|
||||||
|
let checked = false;
|
||||||
|
let freeformId = "freeform_" + i;
|
||||||
|
let radioName = "answer_to_" + i;
|
||||||
|
|
||||||
|
for (let j = 0; j < choices.length; j++) {
|
||||||
|
let newRadio = document.createElement("input");
|
||||||
|
newRadio.setAttribute("type", "radio");
|
||||||
|
newRadio.setAttribute("name", radioName);
|
||||||
|
newRadio.setAttribute("value", j);
|
||||||
|
newRadio.addEventListener(
|
||||||
|
"click", function() {
|
||||||
|
let inputBox = document.getElementById(freeformId);
|
||||||
|
if (inputBox) {
|
||||||
|
inputBox.value = "";
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
let label = document.createElement("span");
|
||||||
|
label.innerHTML = choices[j];
|
||||||
|
if (oldAnswers && oldAnswers[i] == String(j)) {
|
||||||
|
newRadio.setAttribute("checked", "true");
|
||||||
|
checked = true;
|
||||||
|
}
|
||||||
|
contentDiv.appendChild(newRadio);
|
||||||
|
contentDiv.appendChild(label);
|
||||||
|
contentDiv.appendChild(document.createElement("br"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text area:
|
||||||
|
if (surveyQuestions[i].free_entry) {
|
||||||
|
let newRadio = document.createElement("input");
|
||||||
|
newRadio.setAttribute("type", "radio");
|
||||||
|
newRadio.setAttribute("name", radioName);
|
||||||
|
newRadio.setAttribute("value", freeformId);
|
||||||
|
let label = document.createElement("span");
|
||||||
|
label.innerHTML = surveyQuestions[i].free_entry + " : ";
|
||||||
|
let inputBox = document.createElement("textarea");
|
||||||
|
inputBox.setAttribute("id", freeformId);
|
||||||
|
inputBox.addEventListener(
|
||||||
|
"keypress", function() {
|
||||||
|
let elements = document.getElementsByName(radioName);
|
||||||
|
for (let j = 0; j < elements.length; j++) {
|
||||||
|
if (elements[j].value == freeformId) {
|
||||||
|
elements[j].checked = true;
|
||||||
|
} else {
|
||||||
|
elements[j].checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
if (oldAnswers && oldAnswers[i] && (oldAnswers[i].length > 0) &&
|
||||||
|
!checked) {
|
||||||
|
newRadio.setAttribute("checked", "true");
|
||||||
|
inputBox.value = oldAnswers[i];
|
||||||
|
}
|
||||||
|
contentDiv.appendChild(newRadio);
|
||||||
|
contentDiv.appendChild(label);
|
||||||
|
contentDiv.appendChild(inputBox);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBuiltinSurveySubmit() {
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
Components.utils.import("resource://testpilot/modules/log4moz.js");
|
||||||
|
let logger = Log4Moz.repository.getLogger("TestPilot.Survey");
|
||||||
|
let eid = getUrlParam("eid");
|
||||||
|
let task = TestPilotSetup.getTaskById(eid);
|
||||||
|
logger.info("Storing survey answers for survey id " + eid);
|
||||||
|
|
||||||
|
// Read all values from form...
|
||||||
|
let answers = [];
|
||||||
|
let surveyQuestions = task.surveyQuestions;
|
||||||
|
let i;
|
||||||
|
for (i = 0; i < surveyQuestions.length; i++) {
|
||||||
|
let elems = document.getElementsByName("answer_to_" + i);
|
||||||
|
let anAnswer = [];
|
||||||
|
for each (let elem in elems) {
|
||||||
|
if (elem.checked && elem.value != ("freeform_" + i)) {
|
||||||
|
anAnswer.push(elem.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let freeEntry = document.getElementById("freeform_" + i);
|
||||||
|
if (freeEntry && freeEntry.value) {
|
||||||
|
anAnswer.push(freeEntry.value);
|
||||||
|
}
|
||||||
|
answers.push(anAnswer);
|
||||||
|
}
|
||||||
|
let surveyResults = { answers: answers };
|
||||||
|
logger.info("Storing survey answers " + JSON.stringify(surveyResults));
|
||||||
|
task.store(surveyResults, function(submitted) {
|
||||||
|
// Reload page to show submitted status:
|
||||||
|
if (submitted) {
|
||||||
|
onBuiltinSurveyLoad();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBuiltinSurveyChangeAnswers() {
|
||||||
|
let eid = getUrlParam("eid");
|
||||||
|
let task = TestPilotSetup.getTaskById(eid);
|
||||||
|
let contentDiv = document.getElementById("survey-contents");
|
||||||
|
|
||||||
|
drawSurveyForm(task, contentDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStrings() {
|
||||||
|
stringBundle =
|
||||||
|
Components.classes["@mozilla.org/intl/stringbundle;1"].
|
||||||
|
getService(Components.interfaces.nsIStringBundleService).
|
||||||
|
createBundle("chrome://testpilot/locale/main.properties");
|
||||||
|
let map = [
|
||||||
|
{ id: "page-title", stringKey: "testpilot.fullBrandName" },
|
||||||
|
{ id: "comments-and-discussions-link",
|
||||||
|
stringKey: "testpilot.page.commentsAndDiscussions" },
|
||||||
|
{ id: "propose-test-link",
|
||||||
|
stringKey: "testpilot.page.proposeATest" },
|
||||||
|
{ id: "testpilot-twitter-link",
|
||||||
|
stringKey: "testpilot.page.testpilotOnTwitter" },
|
||||||
|
{ id: "change-answers",
|
||||||
|
stringKey: "testpilot.surveyPage.changeAnswers" }
|
||||||
|
];
|
||||||
|
let mapLength = map.length;
|
||||||
|
for (let i = 0; i < mapLength; i++) {
|
||||||
|
let entry = map[i];
|
||||||
|
document.getElementById(entry.id).innerHTML =
|
||||||
|
stringBundle.GetStringFromName(entry.stringKey);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||||
|
<title id="page-title"></title>
|
||||||
|
<link rel="stylesheet" type="text/css" media="all"
|
||||||
|
href="chrome://testpilot/skin/css/screen-standalone.css" />
|
||||||
|
<script src="experiment-page.js" type="application/javascript;version=1.8">
|
||||||
|
</script>
|
||||||
|
<script src="survey-generator.js" type="application/javascript;version=1.8">
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body onload="onBuiltinSurveyLoad();">
|
||||||
|
<div id="container">
|
||||||
|
<div id="content-standalone">
|
||||||
|
<div id="intro">
|
||||||
|
<h1 id="survey-title"></h1>
|
||||||
|
<p id="survey-explanation"></p>
|
||||||
|
<div id="survey-contents"></div>
|
||||||
|
<br>
|
||||||
|
<button id="survey-submit" style="display:none"
|
||||||
|
onclick="onBuiltinSurveySubmit();"></button>
|
||||||
|
<button id="change-answers" style="display:none"
|
||||||
|
onclick="onBuiltinSurveyChangeAnswers();"></button>
|
||||||
|
</div>
|
||||||
|
<div id="links">
|
||||||
|
<p class="spacer">
|
||||||
|
<div class="home_callout">
|
||||||
|
<img class="homeIcon"
|
||||||
|
src="chrome://testpilot/skin/images/home_comments.png">
|
||||||
|
<a onclick="
|
||||||
|
openLink('http://groups.google.com/group/mozilla-labs-testpilot');"
|
||||||
|
id="comments-and-discussions-link"></a>
|
||||||
|
</div> <br>
|
||||||
|
<div class="home_callout">
|
||||||
|
<img class="homeIcon"
|
||||||
|
src="chrome://testpilot/skin/images/home_results.png">
|
||||||
|
<a onclick="openLink('https://wiki.mozilla.org/Labs/Test_Pilot');"
|
||||||
|
id="propose-test-link"></a>
|
||||||
|
</div><br>
|
||||||
|
<div class="home_callout">
|
||||||
|
<img class="homeIcon"
|
||||||
|
src="chrome://testpilot/skin/images/home_twitter.png">
|
||||||
|
<a onclick="openLink('http://www.twitter.com/MozTestPilot');"
|
||||||
|
id="testpilot-twitter-link"></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<?xml-stylesheet href="chrome://testpilot/content/browser.css" type="text/css"?>
|
||||||
|
|
||||||
|
<!DOCTYPE overlay [
|
||||||
|
<!ENTITY % testpilotDTD SYSTEM "chrome://testpilot/locale/main.dtd">
|
||||||
|
%testpilotDTD;
|
||||||
|
]>
|
||||||
|
|
||||||
|
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||||
|
<script src="chrome://testpilot/content/browser.js"
|
||||||
|
type="application/x-javascript" />
|
||||||
|
<script src="chrome://testpilot/content/window-utils.js"
|
||||||
|
type="application/x-javascript" />
|
||||||
|
|
||||||
|
<menupopup id="menu_ToolsPopup">
|
||||||
|
<menu id="pilot-menu" insertafter="menu_openAddons" />
|
||||||
|
</menupopup>
|
||||||
|
|
||||||
|
<menu id="pilot-menu" class="menu-iconic" label="&testpilot.brand.label;"
|
||||||
|
insertafter="addonsManager"
|
||||||
|
image="chrome://testpilot/skin/testpilot_16x16.png">
|
||||||
|
<menupopup id="pilot-menu-popup"
|
||||||
|
onpopuphiding="TestPilotMenuUtils.onPopupHiding(event);">
|
||||||
|
<menu id="pilot-notification-settings" label="&testpilot.settings.label;">
|
||||||
|
<menupopup onpopupshowing="TestPilotMenuUtils.updateSubmenu();">
|
||||||
|
<menuitem class="pilot-notify-me-when"
|
||||||
|
label="&testpilot.settings.notifyMeWhen.label;..."
|
||||||
|
disabled="true"/>
|
||||||
|
<menuitem id="pilot-menu-notify-finished"
|
||||||
|
label="&testpilot.settings.readyToSubmit.label;"
|
||||||
|
type="checkbox"
|
||||||
|
oncommand="
|
||||||
|
TestPilotMenuUtils.togglePref('popup.showOnStudyFinished');"/>
|
||||||
|
<menuitem id="pilot-menu-notify-new"
|
||||||
|
label="&testpilot.settings.newStudy.label;" type="checkbox"
|
||||||
|
oncommand="
|
||||||
|
TestPilotMenuUtils.togglePref('popup.showOnNewStudy');"/>
|
||||||
|
<menuitem id="pilot-menu-notify-results"
|
||||||
|
label="&testpilot.settings.hasNewResults.label;"
|
||||||
|
type="checkbox"
|
||||||
|
oncommand="
|
||||||
|
TestPilotMenuUtils.togglePref('popup.showOnNewResults');"/>
|
||||||
|
<menuseparator />
|
||||||
|
<menuitem id="pilot-menu-always-submit-data"
|
||||||
|
label="&testpilot.settings.alwaysSubmitData.label;"
|
||||||
|
type="checkbox"
|
||||||
|
oncommand="
|
||||||
|
TestPilotMenuUtils.togglePref('alwaysSubmitData');"/>
|
||||||
|
</menupopup>
|
||||||
|
</menu>
|
||||||
|
<menuitem label="&testpilot.allStudies.label;..."
|
||||||
|
oncommand="TestPilotWindowUtils.openAllStudiesWindow();"/>
|
||||||
|
<menuitem label="&testpilot.about.label;"
|
||||||
|
oncommand="TestPilotWindowUtils.openHomepage();"/>
|
||||||
|
</menupopup>
|
||||||
|
</menu>
|
||||||
|
|
||||||
|
<statusbar id="status-bar">
|
||||||
|
<statusbarpanel id="pilot-notifications-button"
|
||||||
|
class="statusbarpanel-iconic"
|
||||||
|
insertbefore="security-button"
|
||||||
|
onmousedown="
|
||||||
|
event.preventDefault();
|
||||||
|
TestPilotMenuUtils.onMenuButtonMouseDown();"
|
||||||
|
image="chrome://testpilot/skin/testpilot_16x16.png"/>
|
||||||
|
|
||||||
|
<panel id="pilot-notification-popup" hidden="true" noautofocus="true"
|
||||||
|
level="parent" position="after_start">
|
||||||
|
<vbox class="pilot-notification-popup-container">
|
||||||
|
<hbox class="pilot-notification-toprow">
|
||||||
|
<image id="pilot-notification-icon" />
|
||||||
|
<vbox pack="center">
|
||||||
|
<label id="pilot-notification-title" class="pilot-title" />
|
||||||
|
</vbox>
|
||||||
|
<spacer flex="1" />
|
||||||
|
<vbox pack="start">
|
||||||
|
<image id="pilot-notification-close"
|
||||||
|
tooltiptext="&testpilot.notification.close.tooltip;" />
|
||||||
|
</vbox>
|
||||||
|
</hbox>
|
||||||
|
<description id="pilot-notification-text" />
|
||||||
|
<hbox align="right"><label id="pilot-notification-link" /></hbox>
|
||||||
|
<hbox>
|
||||||
|
<checkbox id="pilot-notification-always-submit-checkbox"
|
||||||
|
label="&testpilot.settings.alwaysSubmitData.label;" />
|
||||||
|
<spacer flex="1" />
|
||||||
|
</hbox>
|
||||||
|
<hbox align="right">
|
||||||
|
<button id="pilot-notification-submit" />
|
||||||
|
</hbox>
|
||||||
|
</vbox>
|
||||||
|
</panel>
|
||||||
|
|
||||||
|
</statusbar>
|
||||||
|
</overlay>
|
|
@ -0,0 +1,105 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Test Pilot.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
* Raymond Lee <jono@appcoast.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
var TestPilotWelcomePage = {
|
||||||
|
surveyId: "basic_panel_survey_2",
|
||||||
|
|
||||||
|
onLoad: function() {
|
||||||
|
// Show link to pilot background survey only if user hasn't already
|
||||||
|
// taken it.
|
||||||
|
Components.utils.import("resource://testpilot/modules/setup.js");
|
||||||
|
Components.utils.import("resource://testpilot/modules/tasks.js");
|
||||||
|
this._setStrings();
|
||||||
|
let survey = TestPilotSetup.getTaskById(this.surveyId);
|
||||||
|
if (!survey) {
|
||||||
|
// Can happen if page loaded before all tasks loaded
|
||||||
|
window.setTimeout(function() { TestPilotWelcomePage.onLoad(); }, 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (survey.status == TaskConstants.STATUS_NEW) {
|
||||||
|
document.getElementById("survey-link-p").setAttribute("style",
|
||||||
|
"display:block");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openPilotSurvey: function() {
|
||||||
|
let url =
|
||||||
|
"chrome://testpilot/content/take-survey.html?eid=" + this.surveyId;
|
||||||
|
TestPilotWindowUtils.openChromeless(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
_setStrings: function() {
|
||||||
|
let stringBundle =
|
||||||
|
Components.classes["@mozilla.org/intl/stringbundle;1"].
|
||||||
|
getService(Components.interfaces.nsIStringBundleService).
|
||||||
|
createBundle("chrome://testpilot/locale/main.properties");
|
||||||
|
let map = [
|
||||||
|
{ id: "page-title", stringKey: "testpilot.fullBrandName" },
|
||||||
|
{ id: "thank-you-text",
|
||||||
|
stringKey: "testpilot.welcomePage.thankYou" },
|
||||||
|
{ id: "getting-started-text",
|
||||||
|
stringKey: "testpilot.welcomePage.gettingStarted" },
|
||||||
|
{ id: "please-take-text",
|
||||||
|
stringKey: "testpilot.welcomePage.pleaseTake" },
|
||||||
|
{ id: "background-survey-text",
|
||||||
|
stringKey: "testpilot.welcomePage.backgroundSurvey" },
|
||||||
|
{ id: "open-studies-window-link",
|
||||||
|
stringKey: "testpilot.welcomePage.clickToOpenStudiesWindow" },
|
||||||
|
{ id: "testpilot-addon-text",
|
||||||
|
stringKey: "testpilot.welcomePage.testpilotAddon" },
|
||||||
|
{ id: "icon-explanation-text",
|
||||||
|
stringKey: "testpilot.welcomePage.iconExplanation" },
|
||||||
|
{ id: "icon-explanation-more-text",
|
||||||
|
stringKey: "testpilot.welcomePage.moreIconExplanation" },
|
||||||
|
{ id: "notification-info-text",
|
||||||
|
stringKey: "testpilot.welcomePage.notificationInfo" },
|
||||||
|
{ id: "copyright-text",
|
||||||
|
stringKey: "testpilot.welcomePage.copyright" },
|
||||||
|
{ id: "privacy-policy-link",
|
||||||
|
stringKey: "testpilot.welcomePage.privacyPolicy" },
|
||||||
|
{ id: "legal-notices-link",
|
||||||
|
stringKey: "testpilot.welcomePage.legalNotices" }
|
||||||
|
];
|
||||||
|
|
||||||
|
let mapLength = map.length;
|
||||||
|
for (let i = 0; i < mapLength; i++) {
|
||||||
|
let entry = map[i];
|
||||||
|
document.getElementById(entry.id).innerHTML =
|
||||||
|
stringBundle.GetStringFromName(entry.stringKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,55 @@
|
||||||
|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xml:lang="en" xmlns="http://www.w3.org/1999/xhtml" lang="en"><head>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||||
|
<title id="page-title"></title>
|
||||||
|
<link rel="stylesheet" type="text/css" media="all" href="screen.css" />
|
||||||
|
<script src="window-utils.js" type="application/javascript;version=1.8">
|
||||||
|
</script>
|
||||||
|
<script src="welcome-page.js" type="application/javascript;version=1.8">
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body onload="TestPilotWelcomePage.onLoad();">
|
||||||
|
<div id="container">
|
||||||
|
<div id="logo"><a href="http://labs.mozilla.com/">
|
||||||
|
<img src="chrome://testpilot/skin/logo.png" border="0"></a>
|
||||||
|
</div>
|
||||||
|
<div id="contentWelcome">
|
||||||
|
<div id="thanks">
|
||||||
|
<h1 id="thank-you-text"></h1>
|
||||||
|
</div>
|
||||||
|
<div id="get-started">
|
||||||
|
<div class="button">
|
||||||
|
<h2 id="getting-started-text"></h2>
|
||||||
|
<p class="embiggened" id="survey-link-p" style="display:none">
|
||||||
|
<span id="please-take-text"></span>
|
||||||
|
<a class="function-link" id="background-survey-text"
|
||||||
|
onclick="TestPilotWelcomePage.openPilotSurvey();"></a>!</p>
|
||||||
|
<p class="embiggened">
|
||||||
|
<a class="function-link" id="open-studies-window-link"
|
||||||
|
onclick="TestPilotWindowUtils.openAllStudiesWindow();"></a>
|
||||||
|
</p>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="icon-explanation">
|
||||||
|
<div class="button">
|
||||||
|
<p><h2 id="testpilot-addon-text"></h2></p>
|
||||||
|
<p class="embiggened">
|
||||||
|
<img src="chrome://testpilot/skin/testpilot_16x16.png">
|
||||||
|
<strong id="icon-explanation-text"></strong>
|
||||||
|
<span id="icon-explanation-more-text"></span>
|
||||||
|
</p>
|
||||||
|
<p class="embiggened" id="notification-info-text"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="footer">
|
||||||
|
<img class="mozLogo" src="chrome://testpilot/skin/mozilla-logo.png">
|
||||||
|
<span id="copyright-text"></span>
|
||||||
|
<a href="http://www.mozilla.com/en-US/privacy-policy.html"
|
||||||
|
id="privacy-policy-link"></a>
|
||||||
|
<a href="http://www.mozilla.com/en-US/about/legal.html"
|
||||||
|
id="legal-notices-link"></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body></html>
|
|
@ -0,0 +1,139 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Test Pilot.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
* Jorge jorge@mozilla.com
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
var TestPilotWindowUtils;
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
const ALL_STUDIES_WINDOW_NAME = "TestPilotAllStudiesWindow";
|
||||||
|
const ALL_STUDIES_WINDOW_TYPE = "extensions:testpilot:all_studies_window";
|
||||||
|
|
||||||
|
TestPilotWindowUtils = {
|
||||||
|
openAllStudiesWindow: function() {
|
||||||
|
// If the window is not already open, open it; but if it is open,
|
||||||
|
// focus it instead.
|
||||||
|
let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].
|
||||||
|
getService(Components.interfaces.nsIWindowMediator);
|
||||||
|
let allStudiesWindow = wm.getMostRecentWindow(ALL_STUDIES_WINDOW_TYPE);
|
||||||
|
|
||||||
|
if (allStudiesWindow) {
|
||||||
|
allStudiesWindow.focus();
|
||||||
|
} else {
|
||||||
|
allStudiesWindow = window.openDialog(
|
||||||
|
"chrome://testpilot/content/all-studies-window.xul",
|
||||||
|
ALL_STUDIES_WINDOW_NAME, "chrome,titlebar,centerscreen,dialog=no");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openInTab: function(url) {
|
||||||
|
let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
||||||
|
.getService(Components.interfaces.nsIWindowMediator);
|
||||||
|
let enumerator = wm.getEnumerator("navigator:browser");
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
while(enumerator.hasMoreElements()) {
|
||||||
|
let win = enumerator.getNext();
|
||||||
|
let tabbrowser = win.getBrowser();
|
||||||
|
|
||||||
|
// Check each tab of this browser instance
|
||||||
|
let numTabs = tabbrowser.browsers.length;
|
||||||
|
for (let i = 0; i < numTabs; i++) {
|
||||||
|
let currentBrowser = tabbrowser.getBrowserAtIndex(i);
|
||||||
|
if (url == currentBrowser.currentURI.spec) {
|
||||||
|
tabbrowser.selectedTab = tabbrowser.tabContainer.childNodes[i];
|
||||||
|
found = true;
|
||||||
|
win.focus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
let win = wm.getMostRecentWindow("navigator:browser");
|
||||||
|
if (win) {
|
||||||
|
let browser = win.getBrowser();
|
||||||
|
let tab = browser.addTab(url);
|
||||||
|
browser.selectedTab = tab;
|
||||||
|
win.focus();
|
||||||
|
} else {
|
||||||
|
window.open(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getCurrentTabUrl: function() {
|
||||||
|
let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
|
||||||
|
.getService(Components.interfaces.nsIWindowMediator);
|
||||||
|
let win = wm.getMostRecentWindow("navigator:browser");
|
||||||
|
let tabbrowser = win.getBrowser();
|
||||||
|
let currentBrowser = tabbrowser.selectedBrowser;
|
||||||
|
return currentBrowser.currentURI.spec;
|
||||||
|
},
|
||||||
|
|
||||||
|
openHomepage: function() {
|
||||||
|
let Application = Cc["@mozilla.org/fuel/application;1"]
|
||||||
|
.getService(Ci.fuelIApplication);
|
||||||
|
let url = Application.prefs.getValue("extensions.testpilot.homepageURL",
|
||||||
|
"");
|
||||||
|
this.openInTab(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
openFeedbackPage: function(aIsHappy) {
|
||||||
|
Components.utils.import("resource://testpilot/modules/feedback.js");
|
||||||
|
FeedbackManager.setCurrUrl(this.getCurrentTabUrl());
|
||||||
|
if (aIsHappy) {
|
||||||
|
this.openInTab(FeedbackManager.happyUrl);
|
||||||
|
} else {
|
||||||
|
this.openInTab(FeedbackManager.sadUrl);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openChromeless: function(url) {
|
||||||
|
/* Make the window smaller and dialog-boxier
|
||||||
|
* Links to discussion group, twitter, etc should open in new
|
||||||
|
* tab in main browser window, if we have these links here at all!!
|
||||||
|
* Maybe just one link to the main Test Pilot website. */
|
||||||
|
|
||||||
|
// TODO this window opening triggers studies' window-open code.
|
||||||
|
// Is that what we want or not?
|
||||||
|
|
||||||
|
let win = window.open(url, "TestPilotStudyDetailWindow",
|
||||||
|
"chrome,centerscreen,resizable=yes,scrollbars=yes," +
|
||||||
|
"status=no,width=1000,height=800");
|
||||||
|
win.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}());
|
|
@ -0,0 +1,20 @@
|
||||||
|
pref("extensions.testpilot.indexFileName", "index.json");
|
||||||
|
|
||||||
|
pref("extensions.testpilot.popup.delayAfterStartup", 180000); // 3 minutes
|
||||||
|
pref("extensions.testpilot.popup.timeBetweenChecks", 86400000); // 24 hours
|
||||||
|
pref("extensions.testpilot.uploadRetryInterval", 3600000); // 1 hour
|
||||||
|
|
||||||
|
pref("extensions.testpilot.popup.showOnNewStudy", false);
|
||||||
|
pref("extensions.testpilot.popup.showOnStudyFinished", true);
|
||||||
|
pref("extensions.testpilot.popup.showOnNewResults", false);
|
||||||
|
pref("extensions.testpilot.alwaysSubmitData", false);
|
||||||
|
pref("extensions.testpilot.runStudies", true);
|
||||||
|
|
||||||
|
pref("extensions.testpilot.indexBaseURL", "https://testpilot.mozillalabs.com/testcases/");
|
||||||
|
pref("extensions.testpilot.firstRunUrl", "chrome://testpilot/content/welcome.html");
|
||||||
|
pref("extensions.testpilot.dataUploadURL", "https://testpilot.mozillalabs.com/submit/");
|
||||||
|
pref("extensions.testpilot.homepageURL", "https://testpilot.mozillalabs.com/");
|
||||||
|
|
||||||
|
|
||||||
|
pref("extensions.input.happyURL", "http://input.mozilla.com/happy");
|
||||||
|
pref("extensions.input.sadURL", "http://input.mozilla.com/sad");
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
|
||||||
|
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||||
|
<Description about="urn:mozilla:install-manifest">
|
||||||
|
<em:id>testpilot@labs.mozilla.com</em:id>
|
||||||
|
<em:version>1.0rc1</em:version>
|
||||||
|
<em:type>2</em:type>
|
||||||
|
|
||||||
|
<!-- Target Application this extension can install into,
|
||||||
|
with minimum and maximum supported versions. -->
|
||||||
|
<em:targetApplication>
|
||||||
|
<Description>
|
||||||
|
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
|
||||||
|
<em:minVersion>3.5</em:minVersion>
|
||||||
|
<em:maxVersion>4.0b1</em:maxVersion>
|
||||||
|
</Description>
|
||||||
|
</em:targetApplication>
|
||||||
|
|
||||||
|
<!-- Front End MetaData -->
|
||||||
|
<em:name>Feedback</em:name>
|
||||||
|
<em:description>Help make Firefox better by giving feedback.</em:description>
|
||||||
|
<em:creator>Mozilla Corporation</em:creator>
|
||||||
|
<em:homepageURL>http://testpilot.mozillalabs.com/</em:homepageURL>
|
||||||
|
<em:iconURL>chrome://testpilot/skin/dino_32x32.png</em:iconURL>
|
||||||
|
</Description>
|
||||||
|
</RDF>
|
|
@ -0,0 +1,3 @@
|
||||||
|
resource instrument .
|
||||||
|
content instrument .
|
||||||
|
overlay chrome://browser/content chrome://instrument/content
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||||
|
<Description about="urn:mozilla:install-manifest">
|
||||||
|
<em:name>Instrument Test</em:name>
|
||||||
|
<em:id>instrument.test@agadak.net</em:id>
|
||||||
|
<em:version>1</em:version>
|
||||||
|
<em:creator>Edward Lee (Mardak)</em:creator>
|
||||||
|
<em:description>Instrument various browser stuff</em:description>
|
||||||
|
<em:homepageURL>http://ed.agadak.net/</em:homepageURL>
|
||||||
|
<em:targetApplication>
|
||||||
|
<Description>
|
||||||
|
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
|
||||||
|
<em:minVersion>3.0</em:minVersion>
|
||||||
|
<em:maxVersion>3.6a1pre</em:maxVersion>
|
||||||
|
</Description>
|
||||||
|
</em:targetApplication>
|
||||||
|
</Description>
|
||||||
|
</RDF>
|
|
@ -0,0 +1,44 @@
|
||||||
|
let EXPORTED_SYMBOLS = ["Instrument"];
|
||||||
|
|
||||||
|
let data = {};
|
||||||
|
/**
|
||||||
|
* Track how many times an object's member function is called
|
||||||
|
*
|
||||||
|
* @param obj
|
||||||
|
* Object containing the method to track
|
||||||
|
* @param func
|
||||||
|
* Property name of the object that is the function to track
|
||||||
|
* @param name
|
||||||
|
* "Pretty" name to log the usage counts
|
||||||
|
*/
|
||||||
|
let track = function(obj, func, name) {
|
||||||
|
// Initialize count data
|
||||||
|
data[name] = 0;
|
||||||
|
|
||||||
|
// Save the original function to call
|
||||||
|
let orig = obj[func];
|
||||||
|
obj[func] = function() {
|
||||||
|
// Increment our counter right away (in-case there's an exception)
|
||||||
|
data[name]++;
|
||||||
|
|
||||||
|
// Make the call just like it was originally was called
|
||||||
|
return orig.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrument a browser window for various behavior
|
||||||
|
*
|
||||||
|
* @param window
|
||||||
|
* Browser window to instrument
|
||||||
|
*/
|
||||||
|
function Instrument(window) {
|
||||||
|
let $ = function(id) window.document.getElementById(id);
|
||||||
|
|
||||||
|
track(window.gURLBar, "showHistoryPopup", "dropdown");
|
||||||
|
track($("back-button"), "_handleClick", "back");
|
||||||
|
track($("forward-button"), "_handleClick", "forward");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide a way to get at the collected data (e.g., from Error Console)
|
||||||
|
Instrument.report = function() JSON.stringify(data);
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||||
|
<script type="application/javascript">
|
||||||
|
<![CDATA[
|
||||||
|
addEventListener("load", function() {
|
||||||
|
let I = {};
|
||||||
|
Components.utils.import("resource://instrument/instrument.jsm", I);
|
||||||
|
I.Instrument(window);
|
||||||
|
}, false);
|
||||||
|
]]>
|
||||||
|
</script>
|
||||||
|
</overlay>
|
|
@ -0,0 +1,183 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Observers.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Daniel Aquino.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2008
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Daniel Aquino <mr.danielaquino@gmail.com>
|
||||||
|
* Myk Melez <myk@mozilla.org>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
let EXPORTED_SYMBOLS = ["Observers"];
|
||||||
|
|
||||||
|
const Cc = Components.classes;
|
||||||
|
const Ci = Components.interfaces;
|
||||||
|
const Cr = Components.results;
|
||||||
|
const Cu = Components.utils;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service for adding, removing and notifying observers of notifications.
|
||||||
|
* Wraps the nsIObserverService interface.
|
||||||
|
*
|
||||||
|
* @version 0.2
|
||||||
|
*/
|
||||||
|
let Observers = {
|
||||||
|
/**
|
||||||
|
* Register the given callback as an observer of the given topic.
|
||||||
|
*
|
||||||
|
* @param topic {String}
|
||||||
|
* the topic to observe
|
||||||
|
*
|
||||||
|
* @param callback {Object}
|
||||||
|
* the callback; an Object that implements nsIObserver or a Function
|
||||||
|
* that gets called when the notification occurs
|
||||||
|
*
|
||||||
|
* @param thisObject {Object} [optional]
|
||||||
|
* the object to use as |this| when calling a Function callback
|
||||||
|
*
|
||||||
|
* @returns the observer
|
||||||
|
*/
|
||||||
|
add: function(topic, callback, thisObject) {
|
||||||
|
let observer = new Observer(topic, callback, thisObject);
|
||||||
|
this._cache.push(observer);
|
||||||
|
this._service.addObserver(observer, topic, true);
|
||||||
|
|
||||||
|
return observer;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister the given callback as an observer of the given topic.
|
||||||
|
*
|
||||||
|
* @param topic {String}
|
||||||
|
* the topic being observed
|
||||||
|
*
|
||||||
|
* @param callback {Object}
|
||||||
|
* the callback doing the observing
|
||||||
|
*
|
||||||
|
* @param thisObject {Object} [optional]
|
||||||
|
* the object being used as |this| when calling a Function callback
|
||||||
|
*/
|
||||||
|
remove: function(topic, callback, thisObject) {
|
||||||
|
// This seems fairly inefficient, but I'm not sure how much better
|
||||||
|
// we can make it. We could index by topic, but we can't index by callback
|
||||||
|
// or thisObject, as far as I know, since the keys to JavaScript hashes
|
||||||
|
// (a.k.a. objects) can apparently only be primitive values.
|
||||||
|
let [observer] = this._cache.filter(function(v) v.topic == topic &&
|
||||||
|
v.callback == callback &&
|
||||||
|
v.thisObject == thisObject);
|
||||||
|
if (observer) {
|
||||||
|
this._service.removeObserver(observer, topic);
|
||||||
|
this._cache.splice(this._cache.indexOf(observer), 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify observers about something.
|
||||||
|
*
|
||||||
|
* @param topic {String}
|
||||||
|
* the topic to notify observers about
|
||||||
|
*
|
||||||
|
* @param subject {Object} [optional]
|
||||||
|
* some information about the topic; can be any JS object or primitive
|
||||||
|
*
|
||||||
|
* @param data {String} [optional] [deprecated]
|
||||||
|
* some more information about the topic; deprecated as the subject
|
||||||
|
* is sufficient to pass all needed information to the JS observers
|
||||||
|
* that this module targets; if you have multiple values to pass to
|
||||||
|
* the observer, wrap them in an object and pass them via the subject
|
||||||
|
* parameter (i.e.: { foo: 1, bar: "some string", baz: myObject })
|
||||||
|
*/
|
||||||
|
notify: function(topic, subject, data) {
|
||||||
|
subject = (typeof subject == "undefined") ? null : new Subject(subject);
|
||||||
|
data = (typeof data == "undefined") ? null : data;
|
||||||
|
this._service.notifyObservers(subject, topic, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
_service: Cc["@mozilla.org/observer-service;1"].
|
||||||
|
getService(Ci.nsIObserverService),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cache of observers that have been added.
|
||||||
|
*
|
||||||
|
* We use this to remove observers when a caller calls |remove|.
|
||||||
|
*
|
||||||
|
* XXX This might result in reference cycles, causing memory leaks,
|
||||||
|
* if we hold a reference to an observer that holds a reference to us.
|
||||||
|
* Could we fix that by making this an independent top-level object
|
||||||
|
* rather than a property of this object?
|
||||||
|
*/
|
||||||
|
_cache: []
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function Observer(topic, callback, thisObject) {
|
||||||
|
this.topic = topic;
|
||||||
|
this.callback = callback;
|
||||||
|
this.thisObject = thisObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
Observer.prototype = {
|
||||||
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
|
||||||
|
observe: function(subject, topic, data) {
|
||||||
|
// Extract the wrapped object for subjects that are one of our wrappers
|
||||||
|
// around a JS object. This way we support both wrapped subjects created
|
||||||
|
// using this module and those that are real XPCOM components.
|
||||||
|
if (subject && typeof subject == "object" &&
|
||||||
|
("wrappedJSObject" in subject) &&
|
||||||
|
("observersModuleSubjectWrapper" in subject.wrappedJSObject))
|
||||||
|
subject = subject.wrappedJSObject.object;
|
||||||
|
|
||||||
|
if (typeof this.callback == "function") {
|
||||||
|
if (this.thisObject)
|
||||||
|
this.callback.call(this.thisObject, subject, data);
|
||||||
|
else
|
||||||
|
this.callback(subject, data);
|
||||||
|
}
|
||||||
|
else // typeof this.callback == "object" (nsIObserver)
|
||||||
|
this.callback.observe(subject, topic, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Subject(object) {
|
||||||
|
// Double-wrap the object and set a property identifying the wrappedJSObject
|
||||||
|
// as one of our wrappers to distinguish between subjects that are one of our
|
||||||
|
// wrappers (which we should unwrap when notifying our observers) and those
|
||||||
|
// that are real JS XPCOM components (which we should pass through unaltered).
|
||||||
|
this.wrappedJSObject = { observersModuleSubjectWrapper: true, object: object };
|
||||||
|
}
|
||||||
|
|
||||||
|
Subject.prototype = {
|
||||||
|
QueryInterface: XPCOMUtils.generateQI([]),
|
||||||
|
getHelperForLanguage: function() {},
|
||||||
|
getInterfaces: function() {}
|
||||||
|
};
|
|
@ -0,0 +1,96 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Ubiquity.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Brandon Pung <brandonpung@gmail.com>
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
var Ci = Components.interfaces;
|
||||||
|
var Cc = Components.classes;
|
||||||
|
var Cu = Components.utils;
|
||||||
|
var EXPORTED_SYMBOLS = ["DbUtils"];
|
||||||
|
|
||||||
|
Cu.import("resource://testpilot/modules/log4moz.js");
|
||||||
|
|
||||||
|
/* Make a namespace object called DbUtils, to export,
|
||||||
|
* which contains each function in this file.*/
|
||||||
|
var DbUtils = ([f for each (f in this) if (typeof f === "function")]
|
||||||
|
.reduce(function(o, f)(o[f.name] = f, o), {}));
|
||||||
|
|
||||||
|
var _dirSvc = Cc["@mozilla.org/file/directory_service;1"]
|
||||||
|
.getService(Ci.nsIProperties);
|
||||||
|
var _storSvc = Cc["@mozilla.org/storage/service;1"]
|
||||||
|
.getService(Ci.mozIStorageService);
|
||||||
|
|
||||||
|
DbUtils.openDatabase = function openDatabase(file) {
|
||||||
|
/* If the pointed-at file doesn't already exist, it means the database
|
||||||
|
* has never been initialized */
|
||||||
|
let logger = Log4Moz.repository.getLogger("TestPilot.Database");
|
||||||
|
let connection = null;
|
||||||
|
try {
|
||||||
|
logger.debug("Trying to open file...\n");
|
||||||
|
connection = _storSvc.openDatabase(file);
|
||||||
|
logger.debug("Opening file done...\n");
|
||||||
|
} catch(e) {
|
||||||
|
logger.debug("Opening file failed...\n");
|
||||||
|
Components.utils.reportError(
|
||||||
|
"Opening database failed, database may not have been initialized");
|
||||||
|
}
|
||||||
|
return connection;
|
||||||
|
};
|
||||||
|
|
||||||
|
DbUtils.createTable = function createTable(connection, tableName, schema){
|
||||||
|
let logger = Log4Moz.repository.getLogger("TestPilot.Database");
|
||||||
|
let file = connection.databaseFile;
|
||||||
|
logger.debug("File is " + file + "\n");
|
||||||
|
try{
|
||||||
|
if(!connection.tableExists(tableName)){
|
||||||
|
connection.executeSimpleSQL(schema);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
logger.debug("database table: " + tableName + " already exists\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
logger.warn("Error creating database: " + e + "\n");
|
||||||
|
Cu.reportError("Test Pilot's " + tableName +
|
||||||
|
" database table appears to be corrupt, resetting it.");
|
||||||
|
if(file.exists()){
|
||||||
|
//remove corrupt database table
|
||||||
|
file.remove(false);
|
||||||
|
}
|
||||||
|
connection = _storSvc.openDatabase(file);
|
||||||
|
connection.executeSimpleSQL(schema);
|
||||||
|
}
|
||||||
|
return connection;
|
||||||
|
};
|
|
@ -0,0 +1,334 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Test Pilot.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
* Raymond Lee <raymond@appcoast.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
EXPORTED_SYMBOLS = ["ExperimentDataStore", "TYPE_INT_32", "TYPE_DOUBLE",
|
||||||
|
"TYPE_STRING"];
|
||||||
|
|
||||||
|
const Cc = Components.classes;
|
||||||
|
const Ci = Components.interfaces;
|
||||||
|
const Cu = Components.utils;
|
||||||
|
|
||||||
|
Cu.import("resource://testpilot/modules/dbutils.js");
|
||||||
|
Cu.import("resource://testpilot/modules/log4moz.js");
|
||||||
|
Cu.import("resource://testpilot/modules/string_sanitizer.js");
|
||||||
|
var _dirSvc = Cc["@mozilla.org/file/directory_service;1"]
|
||||||
|
.getService(Ci.nsIProperties);
|
||||||
|
var _storSvc = Cc["@mozilla.org/storage/service;1"]
|
||||||
|
.getService(Ci.mozIStorageService);
|
||||||
|
|
||||||
|
const TYPE_INT_32 = 0;
|
||||||
|
const TYPE_DOUBLE = 1;
|
||||||
|
const TYPE_STRING = 2;
|
||||||
|
|
||||||
|
function ExperimentDataStore(fileName, tableName, columns) {
|
||||||
|
this._init(fileName, tableName, columns);
|
||||||
|
}
|
||||||
|
ExperimentDataStore.prototype = {
|
||||||
|
_init: function EDS__init(fileName, tableName, columns) {
|
||||||
|
this._fileName = fileName;
|
||||||
|
this._tableName = tableName;
|
||||||
|
this._columns = columns;
|
||||||
|
let logger = Log4Moz.repository.getLogger("TestPilot.Database");
|
||||||
|
let file = _dirSvc.get("ProfD", Ci.nsIFile);
|
||||||
|
file.append(this._fileName);
|
||||||
|
// openDatabase creates the file if it's not there yet:
|
||||||
|
this._connection = DbUtils.openDatabase(file);
|
||||||
|
// Create schema based on columns:
|
||||||
|
let schemaClauses = [];
|
||||||
|
for (let i = 0; i < this._columns.length; i++) {
|
||||||
|
let colName = this._columns[i].property;
|
||||||
|
let colType;
|
||||||
|
switch( this._columns[i].type) {
|
||||||
|
case TYPE_INT_32: case TYPE_DOUBLE:
|
||||||
|
colType = "INTEGER";
|
||||||
|
break;
|
||||||
|
case TYPE_STRING:
|
||||||
|
colType = "TEXT";
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
schemaClauses.push( colName + " " + colType );
|
||||||
|
}
|
||||||
|
let schema = "CREATE TABLE " + this._tableName + "("
|
||||||
|
+ schemaClauses.join(", ") + ");";
|
||||||
|
// CreateTable creates the table only if it does not already exist:
|
||||||
|
try {
|
||||||
|
this._connection = DbUtils.createTable(this._connection,
|
||||||
|
this._tableName,
|
||||||
|
schema);
|
||||||
|
} catch(e) {
|
||||||
|
logger.warn("Error in createTable: " + e + "\n");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_createStatement: function _createStatement(selectSql) {
|
||||||
|
try {
|
||||||
|
var selStmt = this._connection.createStatement(selectSql);
|
||||||
|
return selStmt;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(this._connection.lastErrorString);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
storeEvent: function EDS_storeEvent(uiEvent, callback) {
|
||||||
|
// Stores event asynchronously; callback will be called back with
|
||||||
|
// true or false on success or failure.
|
||||||
|
let columnNumbers = [];
|
||||||
|
for (let i = 1; i <= this._columns.length; i++) {
|
||||||
|
// the i = 1 is so that we'll start with 1 instead of 0... we want
|
||||||
|
// a string like "...VALUES (?1, ?2, ?3)"
|
||||||
|
columnNumbers.push( "?" + i);
|
||||||
|
}
|
||||||
|
let insertSql = "INSERT INTO " + this._tableName + " VALUES (";
|
||||||
|
insertSql += columnNumbers.join(", ") + ")";
|
||||||
|
let insStmt = this._createStatement(insertSql);
|
||||||
|
for (i = 0; i < this._columns.length; i++) {
|
||||||
|
let datum = uiEvent[this._columns[i].property];
|
||||||
|
switch (this._columns[i].type) {
|
||||||
|
case TYPE_INT_32:
|
||||||
|
insStmt.bindInt32Parameter(i, datum);
|
||||||
|
break;
|
||||||
|
case TYPE_DOUBLE:
|
||||||
|
insStmt.bindDoubleParameter(i, datum);
|
||||||
|
break;
|
||||||
|
case TYPE_STRING:
|
||||||
|
insStmt.bindUTF8StringParameter(i, sanitizeString(datum));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
insStmt.executeAsync({
|
||||||
|
handleResult: function(aResultSet) {
|
||||||
|
},
|
||||||
|
handleError: function(aError) {
|
||||||
|
if (callback) {
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleCompletion: function(aReason) {
|
||||||
|
if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
|
||||||
|
if (callback) {
|
||||||
|
callback(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (callback) {
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
insStmt.finalize();
|
||||||
|
},
|
||||||
|
|
||||||
|
getJSONRows: function EDS_getJSONRows(callback) {
|
||||||
|
let selectSql = "SELECT * FROM " + this._tableName;
|
||||||
|
let selStmt = this._createStatement(selectSql);
|
||||||
|
let records = [];
|
||||||
|
let self = this;
|
||||||
|
let numCols = selStmt.columnCount;
|
||||||
|
|
||||||
|
selStmt.executeAsync({
|
||||||
|
handleResult: function(aResultSet) {
|
||||||
|
for (let row = aResultSet.getNextRow(); row;
|
||||||
|
row = aResultSet.getNextRow()) {
|
||||||
|
let newRecord = [];
|
||||||
|
for (let i = 0; i < numCols; i++) {
|
||||||
|
let column = self._columns[i];
|
||||||
|
//let value = row.getResultByIndex(i);
|
||||||
|
let value = 0;
|
||||||
|
switch (column.type) {
|
||||||
|
case TYPE_INT_32:
|
||||||
|
value = row.getInt32(i);
|
||||||
|
break;
|
||||||
|
case TYPE_DOUBLE:
|
||||||
|
value = row.getDouble(i);
|
||||||
|
break;
|
||||||
|
case TYPE_STRING:
|
||||||
|
value = sanitizeString(row.getUTF8String(i));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
newRecord.push(value);
|
||||||
|
}
|
||||||
|
records.push(newRecord);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleError: function(aError) {
|
||||||
|
callback(records);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCompletion: function(aReason) {
|
||||||
|
callback(records);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
selStmt.finalize();
|
||||||
|
},
|
||||||
|
|
||||||
|
getAllDataAsJSON: function EDS_getAllDataAsJSON( useDisplayValues, callback ) {
|
||||||
|
/* if useDisplayValues is true, the values in the returned JSON are translated to
|
||||||
|
* their human-readable equivalents, using the mechanism provided in the columns
|
||||||
|
* set. If it's false, the values in the returned JSON are straight numerical
|
||||||
|
* values. */
|
||||||
|
|
||||||
|
// Note this works without knowing what the schema is
|
||||||
|
let selectSql = "SELECT * FROM " + this._tableName;
|
||||||
|
let selStmt = this._createStatement(selectSql);
|
||||||
|
let records = [];
|
||||||
|
let self = this;
|
||||||
|
let numCols = selStmt.columnCount;
|
||||||
|
|
||||||
|
selStmt.executeAsync({
|
||||||
|
handleResult: function(aResultSet) {
|
||||||
|
for (let row = aResultSet.getNextRow(); row;
|
||||||
|
row = aResultSet.getNextRow()) {
|
||||||
|
let newRecord = {};
|
||||||
|
for (let i = 0; i < numCols; i++) {
|
||||||
|
let column = self._columns[i];
|
||||||
|
//let value = row.getResultByIndex(i);
|
||||||
|
let value = 0;
|
||||||
|
switch (column.type) {
|
||||||
|
case TYPE_INT_32:
|
||||||
|
value = row.getInt32(i);
|
||||||
|
break;
|
||||||
|
case TYPE_DOUBLE:
|
||||||
|
value = row.getDouble(i);
|
||||||
|
break;
|
||||||
|
case TYPE_STRING:
|
||||||
|
value = sanitizeString(row.getUTF8String(i));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
/* The column may have a property called displayValue, which can be either
|
||||||
|
* a function returning a string or an array of strings. If we're called
|
||||||
|
* with useDisplayValues, then take the raw numeric value and either use it as
|
||||||
|
* an index to the array of strings or use it as input to the function in order
|
||||||
|
* to get the human-readable display name of the value. */
|
||||||
|
if (useDisplayValues && column.displayValue != undefined) {
|
||||||
|
if (typeof( column.displayValue) == "function") {
|
||||||
|
newRecord[column.property] = column.displayValue(value);
|
||||||
|
} else {
|
||||||
|
newRecord[column.property] = column.displayValue[value];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newRecord[column.property] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
records.push(newRecord);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleError: function(aError) {
|
||||||
|
callback(records);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCompletion: function(aReason) {
|
||||||
|
callback(records);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
selStmt.finalize();
|
||||||
|
},
|
||||||
|
|
||||||
|
wipeAllData: function EDS_wipeAllData(callback) {
|
||||||
|
let logger = Log4Moz.repository.getLogger("TestPilot.Database");
|
||||||
|
logger.trace("ExperimentDataStore.wipeAllData called.\n");
|
||||||
|
let wipeSql = "DELETE FROM " + this._tableName;
|
||||||
|
let wipeStmt = this._createStatement(wipeSql);
|
||||||
|
wipeStmt.executeAsync({
|
||||||
|
handleResult: function(aResultSet) {
|
||||||
|
},
|
||||||
|
handleError: function(aError) {
|
||||||
|
if (callback) {
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleCompletion: function(aReason) {
|
||||||
|
if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
|
||||||
|
logger.trace("ExperimentDataStore.wipeAllData complete.\n");
|
||||||
|
if (callback) {
|
||||||
|
callback(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (callback) {
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wipeStmt.finalize();
|
||||||
|
},
|
||||||
|
|
||||||
|
nukeTable: function EDS_nukeTable() {
|
||||||
|
// Should never be called, except if schema needs to be changed
|
||||||
|
// during debugging/development.
|
||||||
|
let nuke = this._createStatement("DROP TABLE " + this._tableName);
|
||||||
|
nuke.executeAsync();
|
||||||
|
nuke.finalize();
|
||||||
|
},
|
||||||
|
|
||||||
|
haveData: function EDS_haveData(callback) {
|
||||||
|
let countSql = "SELECT * FROM " + this._tableName;
|
||||||
|
let countStmt = this._createStatement(countSql);
|
||||||
|
let hasData = false;
|
||||||
|
countStmt.executeAsync({
|
||||||
|
handleResult: function(aResultSet) {
|
||||||
|
if (aResultSet.getNextRow()) {
|
||||||
|
hasData = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleError: function(aError) {
|
||||||
|
callback(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCompletion: function(aReason) {
|
||||||
|
if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED &&
|
||||||
|
hasData) {
|
||||||
|
callback(true);
|
||||||
|
} else {
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
countStmt.finalize();
|
||||||
|
},
|
||||||
|
|
||||||
|
getHumanReadableColumnNames: function EDS_getHumanReadableColumnNames() {
|
||||||
|
let i;
|
||||||
|
return [ this._columns[i].displayName for (i in this._columns) ];
|
||||||
|
},
|
||||||
|
|
||||||
|
getPropertyNames: function EDS_getPropertyNames() {
|
||||||
|
let i;
|
||||||
|
return [ this._columns[i].property for (i in this._columns) ];
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,89 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Test Pilot.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Raymond Lee <raymond@appcoast.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
EXPORTED_SYMBOLS = ["TestPilotExtensionUpdate"];
|
||||||
|
|
||||||
|
const Cc = Components.classes;
|
||||||
|
const Ci = Components.interfaces;
|
||||||
|
const Cr = Components.results;
|
||||||
|
|
||||||
|
let TestPilotExtensionUpdate = {
|
||||||
|
check : function(extensionId) {
|
||||||
|
let extensionManager =
|
||||||
|
Cc["@mozilla.org/extensions/manager;1"].
|
||||||
|
getService(Ci.nsIExtensionManager);
|
||||||
|
let listener = new TestPilotExtensionUpdateCheckListener();
|
||||||
|
let items = [extensionManager.getItemForID(extensionId)];
|
||||||
|
extensionManager.update(
|
||||||
|
items, items.length, false, listener,
|
||||||
|
Ci.nsIExtensionManager.UPDATE_WHEN_USER_REQUESTED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function TestPilotExtensionUpdateCheckListener() {
|
||||||
|
}
|
||||||
|
|
||||||
|
TestPilotExtensionUpdateCheckListener.prototype = {
|
||||||
|
_addons: [],
|
||||||
|
|
||||||
|
onUpdateStarted: function() {
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdateEnded: function() {
|
||||||
|
if (this._addons.length > 0) {
|
||||||
|
let extensionManager =
|
||||||
|
Cc["@mozilla.org/extensions/manager;1"].
|
||||||
|
getService(Ci.nsIExtensionManager);
|
||||||
|
let wm =
|
||||||
|
Cc["@mozilla.org/appshell/window-mediator;1"].
|
||||||
|
getService(Ci.nsIWindowMediator);
|
||||||
|
let win = wm.getMostRecentWindow("navigator:browser");
|
||||||
|
|
||||||
|
extensionManager.addDownloads(this._addons, this._addons.length, null);
|
||||||
|
win.BrowserOpenAddonsMgr("extensions");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onAddonUpdateStarted: function(addon) {
|
||||||
|
},
|
||||||
|
|
||||||
|
onAddonUpdateEnded: function(addon, status) {
|
||||||
|
if (status == Ci.nsIAddonUpdateCheckListener.STATUS_UPDATE) {
|
||||||
|
this._addons.push(addon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Test Pilot.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
EXPORTED_SYMBOLS = ["FeedbackManager"];
|
||||||
|
|
||||||
|
const Cc = Components.classes;
|
||||||
|
const Ci = Components.interfaces;
|
||||||
|
|
||||||
|
let Application = Cc["@mozilla.org/fuel/application;1"]
|
||||||
|
.getService(Ci.fuelIApplication);
|
||||||
|
|
||||||
|
var FeedbackManager = {
|
||||||
|
_lastVisitedUrl: null,
|
||||||
|
|
||||||
|
_happyUrl: null,
|
||||||
|
get happyUrl() {
|
||||||
|
if (!this._happyUrl) {
|
||||||
|
this._happyUrl = Application.prefs.getValue("extensions.input.happyURL", "");
|
||||||
|
}
|
||||||
|
return this._happyUrl;
|
||||||
|
},
|
||||||
|
|
||||||
|
_sadUrl: null,
|
||||||
|
get sadUrl() {
|
||||||
|
if (!this._sadUrl) {
|
||||||
|
this._sadUrl = Application.prefs.getValue("extensions.input.sadURL", "");
|
||||||
|
}
|
||||||
|
return this._sadUrl;
|
||||||
|
},
|
||||||
|
|
||||||
|
setCurrUrl: function FeedbackManager_setCurrUrl(url) {
|
||||||
|
this._lastVisitedUrl = url;
|
||||||
|
},
|
||||||
|
|
||||||
|
fillInFeedbackPage: function FeedbackManager_fifp(url, window) {
|
||||||
|
/* If the user activated the happy or sad feedback feature, a page
|
||||||
|
* gets loaded containing an input field id = id_url
|
||||||
|
* Fill this field in with the referring URL.
|
||||||
|
*/
|
||||||
|
if (url == this.happyUrl || url == this.sadUrl) {
|
||||||
|
let tabbrowser = window.getBrowser();
|
||||||
|
let currentBrowser = tabbrowser.selectedBrowser;
|
||||||
|
let document = currentBrowser.contentDocument;
|
||||||
|
let field = document.getElementById("id_url");
|
||||||
|
if (field && this._lastVisitedUrl) {
|
||||||
|
field.value = this._lastVisitedUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,254 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Test Pilot.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
|
||||||
|
function JarStore() {
|
||||||
|
try {
|
||||||
|
let baseDirName = "TestPilotExperimentFiles"; // this should go in pref?
|
||||||
|
this._baseDir = null;
|
||||||
|
this._localOverrides = {}; //override with code for debugging purposes
|
||||||
|
this._index = {}; // tells us which jar file to look in for each module
|
||||||
|
this._lastModified = {}; // tells us when each jar file was last modified
|
||||||
|
this._init( baseDirName );
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Error instantiating jar store: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JarStore.prototype = {
|
||||||
|
|
||||||
|
_init: function( baseDirectory ) {
|
||||||
|
let prefs = require("preferences-service");
|
||||||
|
this._localOverrides = JSON.parse(
|
||||||
|
prefs.get("extensions.testpilot.codeOverride", "{}"));
|
||||||
|
|
||||||
|
let dir = Cc["@mozilla.org/file/directory_service;1"].
|
||||||
|
getService(Ci.nsIProperties).get("ProfD", Ci.nsIFile);
|
||||||
|
dir.append(baseDirectory);
|
||||||
|
this._baseDir = dir;
|
||||||
|
if( !this._baseDir.exists() || !this._baseDir.isDirectory() ) {
|
||||||
|
// if jar storage directory doesn't exist, create it:
|
||||||
|
console.info("Creating: " + this._baseDir.path + "\n");
|
||||||
|
this._baseDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0777);
|
||||||
|
} else {
|
||||||
|
// Process any jar files already on disk from previous runs:
|
||||||
|
// Build lookup index of module->jar file and modified dates
|
||||||
|
let jarFiles = this._baseDir.directoryEntries;
|
||||||
|
while(jarFiles.hasMoreElements()) {
|
||||||
|
let jarFile = jarFiles.getNext().QueryInterface(Ci.nsIFile);
|
||||||
|
// Make sure this is actually a jar file:
|
||||||
|
if (jarFile.leafName.indexOf(".jar") != jarFile.leafName.length - 4) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this._lastModified[jarFile.leafName] = jarFile.lastModifiedTime;
|
||||||
|
this._indexJar(jarFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_indexJar: function(jarFile) {
|
||||||
|
let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
|
||||||
|
.createInstance(Ci.nsIZipReader);
|
||||||
|
zipReader.open(jarFile); // must already be nsIFile
|
||||||
|
let entries = zipReader.findEntries(null);
|
||||||
|
while(entries.hasMore()) {
|
||||||
|
// Find all .js files inside jar file:
|
||||||
|
let entry = entries.getNext();
|
||||||
|
if (entry.indexOf(".js") == entry.length - 3) {
|
||||||
|
// add entry to index
|
||||||
|
let moduleName = entry.slice(0, entry.length - 3);
|
||||||
|
this._index[moduleName] = jarFile.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zipReader.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
_verifyJar: function(jarFile, expectedHash) {
|
||||||
|
// Compare the jar file's hash to the expected hash from the
|
||||||
|
// index file.
|
||||||
|
// from https://developer.mozilla.org/en/nsICryptoHash#Computing_the_Hash_of_a_File
|
||||||
|
console.info("Attempting to verify jarfile vs hash = " + expectedHash);
|
||||||
|
let istream = Cc["@mozilla.org/network/file-input-stream;1"]
|
||||||
|
.createInstance(Ci.nsIFileInputStream);
|
||||||
|
// open for reading
|
||||||
|
istream.init(jarFile, 0x01, 0444, 0);
|
||||||
|
let ch = Cc["@mozilla.org/security/hash;1"]
|
||||||
|
.createInstance(Ci.nsICryptoHash);
|
||||||
|
// Use SHA256, it's more secure than MD5:
|
||||||
|
ch.init(ch.SHA256);
|
||||||
|
// this tells updateFromStream to read the entire file
|
||||||
|
const PR_UINT32_MAX = 0xffffffff;
|
||||||
|
ch.updateFromStream(istream, PR_UINT32_MAX);
|
||||||
|
// pass false here to get binary data back
|
||||||
|
let hash = ch.finish(false);
|
||||||
|
|
||||||
|
// return the two-digit hexadecimal code for a byte
|
||||||
|
function toHexString(charCode)
|
||||||
|
{
|
||||||
|
return ("0" + charCode.toString(16)).slice(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert the binary hash data to a hex string.
|
||||||
|
let s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
|
||||||
|
// s now contains your hash in hex
|
||||||
|
|
||||||
|
return (s == expectedHash);
|
||||||
|
},
|
||||||
|
|
||||||
|
saveJarFile: function( filename, rawData, expectedHash ) {
|
||||||
|
console.info("Saving a JAR file as " + filename + " hash = " + expectedHash);
|
||||||
|
// rawData is a string of binary data representing a jar file
|
||||||
|
try {
|
||||||
|
let jarFile = this._baseDir.clone();
|
||||||
|
// filename may have directories in it; use just the last part
|
||||||
|
jarFile.append(filename.split("/").pop());
|
||||||
|
|
||||||
|
// If a file of that name already exists, remove it!
|
||||||
|
if (jarFile.exists()) {
|
||||||
|
jarFile.remove(false);
|
||||||
|
}
|
||||||
|
// From https://developer.mozilla.org/en/Code_snippets/File_I%2f%2fO#Getting_special_files
|
||||||
|
jarFile.create( Ci.nsIFile.NORMAL_FILE_TYPE, 600);
|
||||||
|
let stream = Cc["@mozilla.org/network/safe-file-output-stream;1"].
|
||||||
|
createInstance(Ci.nsIFileOutputStream);
|
||||||
|
stream.init(jarFile, 0x04 | 0x08 | 0x20, 0600, 0); // readwrite, create, truncate
|
||||||
|
|
||||||
|
stream.write(rawData, rawData.length);
|
||||||
|
if (stream instanceof Ci.nsISafeOutputStream) {
|
||||||
|
stream.finish();
|
||||||
|
} else {
|
||||||
|
stream.close();
|
||||||
|
}
|
||||||
|
// Verify hash; if it's good, index and set last modified time.
|
||||||
|
// If not good, remove it.
|
||||||
|
if (this._verifyJar(jarFile, expectedHash)) {
|
||||||
|
this._indexJar(jarFile);
|
||||||
|
this._lastModified[jarFile.leafName] = jarFile.lastModifiedTime;
|
||||||
|
} else {
|
||||||
|
console.warn("Bad JAR file, doesn't match hash: " + expectedHash);
|
||||||
|
jarFile.remove(false);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.warn("Error in saving jar file: " + e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resolveModule: function(root, path) {
|
||||||
|
// Root will be null if require() was done by absolute path.
|
||||||
|
if (root != null) {
|
||||||
|
// TODO I don't think we need to do anything special here.
|
||||||
|
}
|
||||||
|
// drop ".js" off end of path to get module
|
||||||
|
let module;
|
||||||
|
if (path.indexOf(".js") == path.length - 3) {
|
||||||
|
module = path.slice(0, path.length - 3);
|
||||||
|
} else {
|
||||||
|
module = path;
|
||||||
|
}
|
||||||
|
if (this._index[module]) {
|
||||||
|
let resolvedPath = this._index[module] + "!" + module + ".js";
|
||||||
|
return resolvedPath;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
// must return a path... which gets passed to getFile.
|
||||||
|
},
|
||||||
|
|
||||||
|
getFile: function(path) {
|
||||||
|
// used externally by cuddlefish; takes the path and returns
|
||||||
|
// {contents: data}.
|
||||||
|
if (this._localOverrides[path]) {
|
||||||
|
let code = this._localOverrides[path];
|
||||||
|
return {contents: code};
|
||||||
|
}
|
||||||
|
let parts = path.split("!");
|
||||||
|
let filePath = parts[0];
|
||||||
|
let entryName = parts[1];
|
||||||
|
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
|
||||||
|
file.initWithPath(filePath);
|
||||||
|
return this._readEntryFromJarFile(file, entryName);
|
||||||
|
},
|
||||||
|
|
||||||
|
_readEntryFromJarFile: function(jarFile, entryName) {
|
||||||
|
// Reads out content of entry, without unzipping jar file to disk.
|
||||||
|
// Open the jar file
|
||||||
|
let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
|
||||||
|
.createInstance(Ci.nsIZipReader);
|
||||||
|
zipReader.open(jarFile); // must already be nsIFile
|
||||||
|
let rawStream = zipReader.getInputStream(entryName);
|
||||||
|
let stream = Cc["@mozilla.org/scriptableinputstream;1"].
|
||||||
|
createInstance(Ci.nsIScriptableInputStream);
|
||||||
|
stream.init(rawStream);
|
||||||
|
try {
|
||||||
|
let data = new String();
|
||||||
|
let chunk = {};
|
||||||
|
do {
|
||||||
|
chunk = stream.read(-1);
|
||||||
|
data += chunk;
|
||||||
|
} while (chunk.length > 0);
|
||||||
|
return {contents: data};
|
||||||
|
} catch(e) {
|
||||||
|
console.warn("Error reading entry from jar file: " + e );
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
getFileModifiedDate: function(filename) {
|
||||||
|
// used by remote experiment loader to know whether we have to redownload
|
||||||
|
// a thing or not.
|
||||||
|
filename = filename.split("/").pop();
|
||||||
|
if (this._lastModified[filename]) {
|
||||||
|
return (this._lastModified[filename]);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
listAllFiles: function() {
|
||||||
|
// used by remote experiment loader
|
||||||
|
let x;
|
||||||
|
let list = [x for (x in this._index)];
|
||||||
|
return list;
|
||||||
|
},
|
||||||
|
|
||||||
|
setLocalOverride: function(path, code) {
|
||||||
|
let prefs = require("preferences-service");
|
||||||
|
this._localOverrides[path] = code;
|
||||||
|
prefs.set("extensions.testpilot.codeOverride",
|
||||||
|
JSON.stringify(this._localOverrides));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.JarStore = JarStore;
|
|
@ -0,0 +1,151 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Jetpack.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Atul Varma <atul@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
(function(global) {
|
||||||
|
const Cc = Components.classes;
|
||||||
|
const Ci = Components.interfaces;
|
||||||
|
const Cu = Components.utils;
|
||||||
|
const Cr = Components.results;
|
||||||
|
|
||||||
|
var exports = {};
|
||||||
|
|
||||||
|
// Load the SecurableModule prerequisite.
|
||||||
|
var securableModule;
|
||||||
|
|
||||||
|
if (global.require)
|
||||||
|
// We're being loaded in a SecurableModule.
|
||||||
|
securableModule = require("securable-module");
|
||||||
|
else {
|
||||||
|
var myURI = Components.stack.filename.split(" -> ").slice(-1)[0];
|
||||||
|
var ios = Cc['@mozilla.org/network/io-service;1']
|
||||||
|
.getService(Ci.nsIIOService);
|
||||||
|
var securableModuleURI = ios.newURI("securable-module.js", null,
|
||||||
|
ios.newURI(myURI, null, null));
|
||||||
|
if (securableModuleURI.scheme == "chrome") {
|
||||||
|
// The securable-module module is at a chrome URI, so we can't
|
||||||
|
// simply load it via Cu.import(). Let's assume we're in a
|
||||||
|
// chrome-privileged document and use mozIJSSubScriptLoader.
|
||||||
|
var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
|
||||||
|
.getService(Ci.mozIJSSubScriptLoader);
|
||||||
|
|
||||||
|
// Import the script, don't pollute the global scope.
|
||||||
|
securableModule = {__proto__: global};
|
||||||
|
loader.loadSubScript(securableModuleURI.spec, securableModule);
|
||||||
|
securableModule = securableModule.SecurableModule;
|
||||||
|
} else {
|
||||||
|
securableModule = {};
|
||||||
|
try {
|
||||||
|
Cu.import(securableModuleURI.spec, securableModule);
|
||||||
|
} catch (e if e.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
|
||||||
|
Cu.reportError("Failed to load " + securableModuleURI.spec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unloadLoader() {
|
||||||
|
this.require("unload").send();
|
||||||
|
}
|
||||||
|
|
||||||
|
var cuddlefishSandboxFactory = {
|
||||||
|
createSandbox: function(options) {
|
||||||
|
var filename = options.filename ? options.filename : null;
|
||||||
|
var sandbox = this.__proto__.createSandbox(options);
|
||||||
|
sandbox.defineProperty("__url__", filename);
|
||||||
|
return sandbox;
|
||||||
|
},
|
||||||
|
__proto__: new securableModule.SandboxFactory("system")
|
||||||
|
};
|
||||||
|
|
||||||
|
function CuddlefishModule(loader) {
|
||||||
|
this.parentLoader = loader;
|
||||||
|
this.__proto__ = exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
var Loader = exports.Loader = function Loader(options) {
|
||||||
|
var globals = {Cc: Components.classes,
|
||||||
|
Ci: Components.interfaces,
|
||||||
|
Cu: Components.utils,
|
||||||
|
Cr: Components.results};
|
||||||
|
|
||||||
|
if (options.console)
|
||||||
|
globals.console = options.console;
|
||||||
|
if (options.memory)
|
||||||
|
globals.memory = options.memory;
|
||||||
|
|
||||||
|
var modules = {};
|
||||||
|
var loaderOptions = {rootPath: options.rootPath,
|
||||||
|
rootPaths: options.rootPaths,
|
||||||
|
fs: options.fs,
|
||||||
|
sandboxFactory: cuddlefishSandboxFactory,
|
||||||
|
globals: globals,
|
||||||
|
modules: modules};
|
||||||
|
|
||||||
|
var loader = new securableModule.Loader(loaderOptions);
|
||||||
|
var path = loader.fs.resolveModule(null, "cuddlefish");
|
||||||
|
modules[path] = new CuddlefishModule(loader);
|
||||||
|
|
||||||
|
if (!globals.console) {
|
||||||
|
var console = loader.require("plain-text-console");
|
||||||
|
globals.console = new console.PlainTextConsole(options.print);
|
||||||
|
}
|
||||||
|
if (!globals.memory)
|
||||||
|
globals.memory = loader.require("memory");
|
||||||
|
|
||||||
|
loader.console = globals.console;
|
||||||
|
loader.memory = globals.memory;
|
||||||
|
loader.unload = unloadLoader;
|
||||||
|
|
||||||
|
return loader;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (global.window) {
|
||||||
|
// We're being loaded in a chrome window, or a web page with
|
||||||
|
// UniversalXPConnect privileges.
|
||||||
|
global.Cuddlefish = exports;
|
||||||
|
} else if (global.exports) {
|
||||||
|
// We're being loaded in a SecurableModule.
|
||||||
|
for (name in exports) {
|
||||||
|
global.exports[name] = exports[name];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We're being loaded in a JS module.
|
||||||
|
global.EXPORTED_SYMBOLS = [];
|
||||||
|
for (name in exports) {
|
||||||
|
global.EXPORTED_SYMBOLS.push(name);
|
||||||
|
global[name] = exports[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(this);
|
|
@ -0,0 +1,115 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Jetpack.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Atul Varma <atul@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
var compactTimerId;
|
||||||
|
var COMPACT_INTERVAL = 5000;
|
||||||
|
var trackedObjects = {};
|
||||||
|
|
||||||
|
function scheduleNextCompaction() {
|
||||||
|
compactTimerId = require("timer").setTimeout(compact, COMPACT_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compact() {
|
||||||
|
var newTrackedObjects = {};
|
||||||
|
for (name in trackedObjects) {
|
||||||
|
var oldBin = trackedObjects[name];
|
||||||
|
var newBin = [];
|
||||||
|
var strongRefs = [];
|
||||||
|
for (var i = 0; i < oldBin.length; i++) {
|
||||||
|
var strongRef = oldBin[i].weakref.get();
|
||||||
|
if (strongRef && strongRefs.indexOf(strongRef) == -1) {
|
||||||
|
strongRefs.push(strongRef);
|
||||||
|
newBin.push(oldBin[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newBin.length)
|
||||||
|
newTrackedObjects[name] = newBin;
|
||||||
|
}
|
||||||
|
trackedObjects = newTrackedObjects;
|
||||||
|
scheduleNextCompaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
var track = exports.track = function track(object, bin, stackFrameNumber) {
|
||||||
|
if (!compactTimerId)
|
||||||
|
scheduleNextCompaction();
|
||||||
|
var frame = Components.stack.caller;
|
||||||
|
var weakref = Cu.getWeakReference(object);
|
||||||
|
if (!bin)
|
||||||
|
bin = object.constructor.name;
|
||||||
|
if (bin == "Object")
|
||||||
|
bin = frame.name;
|
||||||
|
if (!bin)
|
||||||
|
bin = "generic";
|
||||||
|
if (!(bin in trackedObjects))
|
||||||
|
trackedObjects[bin] = [];
|
||||||
|
|
||||||
|
if (stackFrameNumber > 0)
|
||||||
|
for (var i = 0; i < stackFrameNumber; i++)
|
||||||
|
frame = frame.caller;
|
||||||
|
|
||||||
|
trackedObjects[bin].push({weakref: weakref,
|
||||||
|
created: new Date(),
|
||||||
|
filename: frame.filename,
|
||||||
|
lineNo: frame.lineNumber});
|
||||||
|
};
|
||||||
|
|
||||||
|
var getBins = exports.getBins = function getBins() {
|
||||||
|
var names = [];
|
||||||
|
for (name in trackedObjects)
|
||||||
|
names.push(name);
|
||||||
|
return names;
|
||||||
|
};
|
||||||
|
|
||||||
|
var getObjects = exports.getObjects = function getObjects(bin) {
|
||||||
|
function getLiveObjectsInBin(bin, array) {
|
||||||
|
for (var i = 0; i < bin.length; i++) {
|
||||||
|
var object = bin[i].weakref.get();
|
||||||
|
if (object)
|
||||||
|
array.push(bin[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = [];
|
||||||
|
if (bin) {
|
||||||
|
if (bin in trackedObjects)
|
||||||
|
getLiveObjectsInBin(trackedObjects[bin], results);
|
||||||
|
} else
|
||||||
|
for (name in trackedObjects)
|
||||||
|
getLiveObjectsInBin(trackedObjects[name], results);
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
require("unload").when(function() { trackedObjects = {}; });
|
|
@ -0,0 +1,191 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Observers.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Daniel Aquino.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2008
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Daniel Aquino <mr.danielaquino@gmail.com>
|
||||||
|
* Myk Melez <myk@mozilla.org>
|
||||||
|
* Atul Varma <atul@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
var jsm = {}; Cu.import("resource://gre/modules/XPCOMUtils.jsm", jsm);
|
||||||
|
var XPCOMUtils = jsm.XPCOMUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service for adding, removing and notifying observers of notifications.
|
||||||
|
* Wraps the nsIObserverService interface.
|
||||||
|
*
|
||||||
|
* @version 0.2
|
||||||
|
*/
|
||||||
|
|
||||||
|
var service = Cc["@mozilla.org/observer-service;1"].
|
||||||
|
getService(Ci.nsIObserverService);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cache of observers that have been added.
|
||||||
|
*
|
||||||
|
* We use this to remove observers when a caller calls |Observers.remove|.
|
||||||
|
*/
|
||||||
|
var cache = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the given callback as an observer of the given topic.
|
||||||
|
*
|
||||||
|
* @param topic {String}
|
||||||
|
* the topic to observe
|
||||||
|
*
|
||||||
|
* @param callback {Object}
|
||||||
|
* the callback; an Object that implements nsIObserver or a Function
|
||||||
|
* that gets called when the notification occurs
|
||||||
|
*
|
||||||
|
* @param thisObject {Object} [optional]
|
||||||
|
* the object to use as |this| when calling a Function callback
|
||||||
|
*
|
||||||
|
* @returns the observer
|
||||||
|
*/
|
||||||
|
var add = exports.add = function add(topic, callback, thisObject) {
|
||||||
|
var observer = new Observer(topic, callback, thisObject);
|
||||||
|
service.addObserver(observer, topic, true);
|
||||||
|
cache.push(observer);
|
||||||
|
|
||||||
|
return observer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister the given callback as an observer of the given topic.
|
||||||
|
*
|
||||||
|
* @param topic {String}
|
||||||
|
* the topic being observed
|
||||||
|
*
|
||||||
|
* @param callback {Object}
|
||||||
|
* the callback doing the observing
|
||||||
|
*
|
||||||
|
* @param thisObject {Object} [optional]
|
||||||
|
* the object being used as |this| when calling a Function callback
|
||||||
|
*/
|
||||||
|
var remove = exports.remove = function remove(topic, callback, thisObject) {
|
||||||
|
// This seems fairly inefficient, but I'm not sure how much better
|
||||||
|
// we can make it. We could index by topic, but we can't index by callback
|
||||||
|
// or thisObject, as far as I know, since the keys to JavaScript hashes
|
||||||
|
// (a.k.a. objects) can apparently only be primitive values.
|
||||||
|
var [observer] = cache.filter(function(v) {
|
||||||
|
return (v.topic == topic &&
|
||||||
|
v.callback == callback &&
|
||||||
|
v.thisObject == thisObject);
|
||||||
|
});
|
||||||
|
if (observer) {
|
||||||
|
service.removeObserver(observer, topic);
|
||||||
|
cache.splice(cache.indexOf(observer), 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify observers about something.
|
||||||
|
*
|
||||||
|
* @param topic {String}
|
||||||
|
* the topic to notify observers about
|
||||||
|
*
|
||||||
|
* @param subject {Object} [optional]
|
||||||
|
* some information about the topic; can be any JS object or primitive
|
||||||
|
*
|
||||||
|
* @param data {String} [optional] [deprecated]
|
||||||
|
* some more information about the topic; deprecated as the subject
|
||||||
|
* is sufficient to pass all needed information to the JS observers
|
||||||
|
* that this module targets; if you have multiple values to pass to
|
||||||
|
* the observer, wrap them in an object and pass them via the subject
|
||||||
|
* parameter (i.e.: { foo: 1, bar: "some string", baz: myObject })
|
||||||
|
*/
|
||||||
|
var notify = exports.notify = function notify(topic, subject, data) {
|
||||||
|
subject = (typeof subject == "undefined") ? null : new Subject(subject);
|
||||||
|
data = (typeof data == "undefined") ? null : data;
|
||||||
|
service.notifyObservers(subject, topic, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
function Observer(topic, callback, thisObject) {
|
||||||
|
this.topic = topic;
|
||||||
|
this.callback = callback;
|
||||||
|
this.thisObject = thisObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
Observer.prototype = {
|
||||||
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
|
||||||
|
Ci.nsISupportsWeakReference]),
|
||||||
|
observe: function(subject, topic, data) {
|
||||||
|
// Extract the wrapped object for subjects that are one of our
|
||||||
|
// wrappers around a JS object. This way we support both wrapped
|
||||||
|
// subjects created using this module and those that are real
|
||||||
|
// XPCOM components.
|
||||||
|
if (subject && typeof subject == "object" &&
|
||||||
|
("wrappedJSObject" in subject) &&
|
||||||
|
("observersModuleSubjectWrapper" in subject.wrappedJSObject))
|
||||||
|
subject = subject.wrappedJSObject.object;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof this.callback == "function") {
|
||||||
|
if (this.thisObject)
|
||||||
|
this.callback.call(this.thisObject, subject, data);
|
||||||
|
else
|
||||||
|
this.callback(subject, data);
|
||||||
|
} else // typeof this.callback == "object" (nsIObserver)
|
||||||
|
this.callback.observe(subject, topic, data);
|
||||||
|
} catch (e) {
|
||||||
|
console.exception(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function Subject(object) {
|
||||||
|
// Double-wrap the object and set a property identifying the
|
||||||
|
// wrappedJSObject as one of our wrappers to distinguish between
|
||||||
|
// subjects that are one of our wrappers (which we should unwrap
|
||||||
|
// when notifying our observers) and those that are real JS XPCOM
|
||||||
|
// components (which we should pass through unaltered).
|
||||||
|
this.wrappedJSObject = {
|
||||||
|
observersModuleSubjectWrapper: true,
|
||||||
|
object: object
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Subject.prototype = {
|
||||||
|
QueryInterface: XPCOMUtils.generateQI([]),
|
||||||
|
getHelperForLanguage: function() {},
|
||||||
|
getInterfaces: function() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
require("unload").when(
|
||||||
|
function removeAllObservers() {
|
||||||
|
// Make a copy of cache first, since cache will be changing as we
|
||||||
|
// iterate through it.
|
||||||
|
cache.slice().forEach(
|
||||||
|
function(observer) {
|
||||||
|
remove(observer.topic, observer.callback, observer.thisObject);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,100 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Jetpack.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Atul Varma <atul@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
function stringify(arg) {
|
||||||
|
try {
|
||||||
|
return String(arg);
|
||||||
|
}
|
||||||
|
catch(ex) {
|
||||||
|
return "<toString() error>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyArgs(args) {
|
||||||
|
return Array.map(args, stringify).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function message(print, level, args) {
|
||||||
|
print(level + ": " + stringifyArgs(args) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
var Console = exports.PlainTextConsole = function PlainTextConsole(print) {
|
||||||
|
if (!print)
|
||||||
|
print = dump;
|
||||||
|
if (print === dump) {
|
||||||
|
// If we're just using dump(), auto-enable preferences so
|
||||||
|
// that the developer actually sees the console output.
|
||||||
|
var prefs = Cc["@mozilla.org/preferences-service;1"]
|
||||||
|
.getService(Ci.nsIPrefBranch);
|
||||||
|
prefs.setBoolPref("browser.dom.window.dump.enabled", true);
|
||||||
|
}
|
||||||
|
this.print = print;
|
||||||
|
};
|
||||||
|
|
||||||
|
Console.prototype = {
|
||||||
|
log: function log() {
|
||||||
|
message(this.print, "info", arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
info: function info() {
|
||||||
|
message(this.print, "info", arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
warn: function warn() {
|
||||||
|
message(this.print, "warning", arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
error: function error() {
|
||||||
|
message(this.print, "error", arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
debug: function debug() {
|
||||||
|
message(this.print, "debug", arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
exception: function exception(e) {
|
||||||
|
var fullString = ("An exception occurred.\n" +
|
||||||
|
require("traceback").format(e) + "\n" + e);
|
||||||
|
this.error(fullString);
|
||||||
|
},
|
||||||
|
|
||||||
|
trace: function trace() {
|
||||||
|
var traceback = require("traceback");
|
||||||
|
var stack = traceback.get();
|
||||||
|
stack.splice(-1, 1);
|
||||||
|
message(this.print, "info", [traceback.format(stack)]);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,138 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Preferences.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2008
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Myk Melez <myk@mozilla.org>
|
||||||
|
* Daniel Aquino <mr.danielaquino@gmail.com>
|
||||||
|
* Atul Varma <atul@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
var jsm = {}; Cu.import("resource://gre/modules/XPCOMUtils.jsm", jsm);
|
||||||
|
var XPCOMUtils = jsm.XPCOMUtils;
|
||||||
|
|
||||||
|
// The minimum and maximum integers that can be set as preferences.
|
||||||
|
// The range of valid values is narrower than the range of valid JS values
|
||||||
|
// because the native preferences code treats integers as NSPR PRInt32s,
|
||||||
|
// which are 32-bit signed integers on all platforms.
|
||||||
|
const MAX_INT = Math.pow(2, 31) - 1;
|
||||||
|
const MIN_INT = -MAX_INT;
|
||||||
|
|
||||||
|
var prefSvc = Cc["@mozilla.org/preferences-service;1"].
|
||||||
|
getService(Ci.nsIPrefService).getBranch(null);
|
||||||
|
|
||||||
|
var get = exports.get = function get(name, defaultValue) {
|
||||||
|
switch (prefSvc.getPrefType(name)) {
|
||||||
|
case Ci.nsIPrefBranch.PREF_STRING:
|
||||||
|
return prefSvc.getComplexValue(name, Ci.nsISupportsString).data;
|
||||||
|
|
||||||
|
case Ci.nsIPrefBranch.PREF_INT:
|
||||||
|
return prefSvc.getIntPref(name);
|
||||||
|
|
||||||
|
case Ci.nsIPrefBranch.PREF_BOOL:
|
||||||
|
return prefSvc.getBoolPref(name);
|
||||||
|
|
||||||
|
case Ci.nsIPrefBranch.PREF_INVALID:
|
||||||
|
return defaultValue;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// This should never happen.
|
||||||
|
throw new Error("Error getting pref " + name +
|
||||||
|
"; its value's type is " +
|
||||||
|
prefSvc.getPrefType(name) +
|
||||||
|
", which I don't know " +
|
||||||
|
"how to handle.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var set = exports.set = function set(name, value) {
|
||||||
|
var prefType;
|
||||||
|
if (typeof value != "undefined" && value != null)
|
||||||
|
prefType = value.constructor.name;
|
||||||
|
|
||||||
|
switch (prefType) {
|
||||||
|
case "String":
|
||||||
|
{
|
||||||
|
var string = Cc["@mozilla.org/supports-string;1"].
|
||||||
|
createInstance(Ci.nsISupportsString);
|
||||||
|
string.data = value;
|
||||||
|
prefSvc.setComplexValue(name, Ci.nsISupportsString, string);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Number":
|
||||||
|
// We throw if the number is outside the range, since the result
|
||||||
|
// will never be what the consumer wanted to store, but we only warn
|
||||||
|
// if the number is non-integer, since the consumer might not mind
|
||||||
|
// the loss of precision.
|
||||||
|
if (value > MAX_INT || value < MIN_INT)
|
||||||
|
throw new Error("you cannot set the " + name +
|
||||||
|
" pref to the number " + value +
|
||||||
|
", as number pref values must be in the signed " +
|
||||||
|
"32-bit integer range -(2^31-1) to 2^31-1. " +
|
||||||
|
"To store numbers outside that range, store " +
|
||||||
|
"them as strings.");
|
||||||
|
if (value % 1 != 0)
|
||||||
|
throw new Error("cannot store non-integer number: " + value);
|
||||||
|
prefSvc.setIntPref(name, value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Boolean":
|
||||||
|
prefSvc.setBoolPref(name, value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error("can't set pref " + name + " to value '" + value +
|
||||||
|
"'; it isn't a String, Number, or Boolean");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var has = exports.has = function has(name) {
|
||||||
|
return (prefSvc.getPrefType(name) != Ci.nsIPrefBranch.PREF_INVALID);
|
||||||
|
};
|
||||||
|
|
||||||
|
var isSet = exports.isSet = function isSet(name) {
|
||||||
|
return (has(name) && prefSvc.prefHasUserValue(name));
|
||||||
|
};
|
||||||
|
|
||||||
|
var reset = exports.reset = function reset(name) {
|
||||||
|
try {
|
||||||
|
prefSvc.clearUserPref(name);
|
||||||
|
} catch (e if e.result == Cr.NS_ERROR_UNEXPECTED) {
|
||||||
|
// The pref service throws NS_ERROR_UNEXPECTED when the caller tries
|
||||||
|
// to reset a pref that doesn't exist or is already set to its default
|
||||||
|
// value. This interface fails silently in those cases, so callers
|
||||||
|
// can unconditionally reset a pref without having to check if it needs
|
||||||
|
// resetting first or trap exceptions after the fact. It passes through
|
||||||
|
// other exceptions, however, so callers know about them, since we don't
|
||||||
|
// know what other exceptions might be thrown and what they might mean.
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,320 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Jetpack.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Atul Varma <atul@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
(function(global) {
|
||||||
|
const Cc = Components.classes;
|
||||||
|
const Ci = Components.interfaces;
|
||||||
|
const Cu = Components.utils;
|
||||||
|
const Cr = Components.results;
|
||||||
|
|
||||||
|
var exports = {};
|
||||||
|
|
||||||
|
var ios = Cc['@mozilla.org/network/io-service;1']
|
||||||
|
.getService(Ci.nsIIOService);
|
||||||
|
|
||||||
|
var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"]
|
||||||
|
.createInstance(Ci.nsIPrincipal);
|
||||||
|
|
||||||
|
function resolvePrincipal(principal, defaultPrincipal) {
|
||||||
|
if (principal === undefined)
|
||||||
|
return defaultPrincipal;
|
||||||
|
if (principal == "system")
|
||||||
|
return systemPrincipal;
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The base URI to we use when we're given relative URLs, if any.
|
||||||
|
var baseURI = null;
|
||||||
|
if (global.window)
|
||||||
|
baseURI = ios.newURI(global.location.href, null, null);
|
||||||
|
exports.baseURI = baseURI;
|
||||||
|
|
||||||
|
// The "parent" chrome URI to use if we're loading code that
|
||||||
|
// needs chrome privileges but may not have a filename that
|
||||||
|
// matches any of SpiderMonkey's defined system filename prefixes.
|
||||||
|
// The latter is needed so that wrappers can be automatically
|
||||||
|
// made for the code. For more information on this, see
|
||||||
|
// bug 418356:
|
||||||
|
//
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=418356
|
||||||
|
var parentChromeURIString;
|
||||||
|
if (baseURI)
|
||||||
|
// We're being loaded from a chrome-privileged document, so
|
||||||
|
// use its URL as the parent string.
|
||||||
|
parentChromeURIString = baseURI.spec;
|
||||||
|
else
|
||||||
|
// We're being loaded from a chrome-privileged JS module or
|
||||||
|
// SecurableModule, so use its filename (which may itself
|
||||||
|
// contain a reference to a parent).
|
||||||
|
parentChromeURIString = Components.stack.filename;
|
||||||
|
|
||||||
|
function maybeParentifyFilename(filename) {
|
||||||
|
var doParentifyFilename = true;
|
||||||
|
try {
|
||||||
|
// TODO: Ideally we should just make
|
||||||
|
// nsIChromeRegistry.wrappersEnabled() available from script
|
||||||
|
// and use it here. Until that's in the platform, though,
|
||||||
|
// we'll play it safe and parentify the filename unless
|
||||||
|
// we're absolutely certain things will be ok if we don't.
|
||||||
|
var filenameURI = ios.newURI(options.filename,
|
||||||
|
null,
|
||||||
|
baseURI);
|
||||||
|
if (filenameURI.scheme == 'chrome' &&
|
||||||
|
filenameURI.path.indexOf('/content/') == 0)
|
||||||
|
// Content packages will always have wrappers made for them;
|
||||||
|
// if automatic wrappers have been disabled for the
|
||||||
|
// chrome package via a chrome manifest flag, then
|
||||||
|
// this still works too, to the extent that the
|
||||||
|
// content package is insecure anyways.
|
||||||
|
doParentifyFilename = false;
|
||||||
|
} catch (e) {}
|
||||||
|
if (doParentifyFilename)
|
||||||
|
return parentChromeURIString + " -> " + filename;
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRootDir(urlStr) {
|
||||||
|
// TODO: This feels hacky, and like there will be edge cases.
|
||||||
|
return urlStr.slice(0, urlStr.lastIndexOf("/") + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.SandboxFactory = function SandboxFactory(defaultPrincipal) {
|
||||||
|
// Unless specified otherwise, use a principal with limited
|
||||||
|
// privileges.
|
||||||
|
this._defaultPrincipal = resolvePrincipal(defaultPrincipal,
|
||||||
|
"http://www.mozilla.org");
|
||||||
|
},
|
||||||
|
|
||||||
|
exports.SandboxFactory.prototype = {
|
||||||
|
createSandbox: function createSandbox(options) {
|
||||||
|
var principal = resolvePrincipal(options.principal,
|
||||||
|
this._defaultPrincipal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
_sandbox: new Cu.Sandbox(principal),
|
||||||
|
_principal: principal,
|
||||||
|
defineProperty: function defineProperty(name, value) {
|
||||||
|
this._sandbox[name] = value;
|
||||||
|
},
|
||||||
|
evaluate: function evaluate(options) {
|
||||||
|
if (typeof(options) == 'string')
|
||||||
|
options = {contents: options};
|
||||||
|
options = {__proto__: options};
|
||||||
|
if (typeof(options.contents) != 'string')
|
||||||
|
throw new Error('Expected string for options.contents');
|
||||||
|
if (options.lineNo === undefined)
|
||||||
|
options.lineNo = 1;
|
||||||
|
if (options.jsVersion === undefined)
|
||||||
|
options.jsVersion = "1.8";
|
||||||
|
if (typeof(options.filename) != 'string')
|
||||||
|
options.filename = '<string>';
|
||||||
|
|
||||||
|
if (this._principal == systemPrincipal)
|
||||||
|
options.filename = maybeParentifyFilename(options.filename);
|
||||||
|
|
||||||
|
return Cu.evalInSandbox(options.contents,
|
||||||
|
this._sandbox,
|
||||||
|
options.jsVersion,
|
||||||
|
options.filename,
|
||||||
|
options.lineNo);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Loader = function Loader(options) {
|
||||||
|
options = {__proto__: options};
|
||||||
|
if (options.fs === undefined) {
|
||||||
|
var rootPaths = options.rootPath || options.rootPaths;
|
||||||
|
if (rootPaths) {
|
||||||
|
if (rootPaths.constructor.name != "Array")
|
||||||
|
rootPaths = [rootPaths];
|
||||||
|
var fses = [new exports.LocalFileSystem(path)
|
||||||
|
for each (path in rootPaths)];
|
||||||
|
options.fs = new exports.CompositeFileSystem(fses);
|
||||||
|
} else
|
||||||
|
options.fs = new exports.LocalFileSystem();
|
||||||
|
}
|
||||||
|
if (options.sandboxFactory === undefined)
|
||||||
|
options.sandboxFactory = new exports.SandboxFactory(
|
||||||
|
options.defaultPrincipal
|
||||||
|
);
|
||||||
|
if (options.modules === undefined)
|
||||||
|
options.modules = {};
|
||||||
|
if (options.globals === undefined)
|
||||||
|
options.globals = {};
|
||||||
|
|
||||||
|
this.fs = options.fs;
|
||||||
|
this.sandboxFactory = options.sandboxFactory;
|
||||||
|
this.modules = options.modules;
|
||||||
|
this.globals = options.globals;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Loader.prototype = {
|
||||||
|
_makeRequire: function _makeRequire(rootDir) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
return function require(module) {
|
||||||
|
var path = self.fs.resolveModule(rootDir, module);
|
||||||
|
if (!path)
|
||||||
|
throw new Error('Module "' + module + '" not found');
|
||||||
|
if (!(path in self.modules)) {
|
||||||
|
var options = self.fs.getFile(path);
|
||||||
|
if (options.filename === undefined)
|
||||||
|
options.filename = path;
|
||||||
|
|
||||||
|
var exports = {};
|
||||||
|
var sandbox = self.sandboxFactory.createSandbox(options);
|
||||||
|
for (name in self.globals)
|
||||||
|
sandbox.defineProperty(name, self.globals[name]);
|
||||||
|
sandbox.defineProperty('require', self._makeRequire(path));
|
||||||
|
sandbox.defineProperty('exports', exports);
|
||||||
|
self.modules[path] = exports;
|
||||||
|
sandbox.evaluate(options);
|
||||||
|
}
|
||||||
|
return self.modules[path];
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
require: function require(module) {
|
||||||
|
return (this._makeRequire(null))(module);
|
||||||
|
},
|
||||||
|
|
||||||
|
runScript: function runScript(options) {
|
||||||
|
if (typeof(options) == 'string')
|
||||||
|
options = {contents: options};
|
||||||
|
options = {__proto__: options};
|
||||||
|
var sandbox = this.sandboxFactory.createSandbox(options);
|
||||||
|
for (name in this.globals)
|
||||||
|
sandbox.defineProperty(name, this.globals[name]);
|
||||||
|
sandbox.defineProperty('require', this._makeRequire(null));
|
||||||
|
return sandbox.evaluate(options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.CompositeFileSystem = function CompositeFileSystem(fses) {
|
||||||
|
this.fses = fses;
|
||||||
|
this._pathMap = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.CompositeFileSystem.prototype = {
|
||||||
|
resolveModule: function resolveModule(base, path) {
|
||||||
|
for (var i = 0; i < this.fses.length; i++) {
|
||||||
|
var fs = this.fses[i];
|
||||||
|
var absPath = fs.resolveModule(base, path);
|
||||||
|
if (absPath) {
|
||||||
|
this._pathMap[absPath] = fs;
|
||||||
|
return absPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getFile: function getFile(path) {
|
||||||
|
return this._pathMap[path].getFile(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.LocalFileSystem = function LocalFileSystem(root) {
|
||||||
|
if (root === undefined) {
|
||||||
|
if (!baseURI)
|
||||||
|
throw new Error("Need a root path for module filesystem");
|
||||||
|
root = baseURI;
|
||||||
|
}
|
||||||
|
if (typeof(root) == 'string')
|
||||||
|
root = ios.newURI(root, null, baseURI);
|
||||||
|
if (root instanceof Ci.nsIFile)
|
||||||
|
root = ios.newFileURI(root);
|
||||||
|
if (!(root instanceof Ci.nsIURI))
|
||||||
|
throw new Error('Expected nsIFile, nsIURI, or string for root');
|
||||||
|
|
||||||
|
this.root = root.spec;
|
||||||
|
this._rootURI = root;
|
||||||
|
this._rootURIDir = getRootDir(root.spec);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.LocalFileSystem.prototype = {
|
||||||
|
resolveModule: function resolveModule(base, path) {
|
||||||
|
path = path + ".js";
|
||||||
|
|
||||||
|
var baseURI;
|
||||||
|
if (!base || path.charAt(0) != '.')
|
||||||
|
baseURI = this._rootURI;
|
||||||
|
else
|
||||||
|
baseURI = ios.newURI(base, null, null);
|
||||||
|
var newURI = ios.newURI(path, null, baseURI);
|
||||||
|
if (newURI.spec.indexOf(this._rootURIDir) == 0) {
|
||||||
|
var channel = ios.newChannelFromURI(newURI);
|
||||||
|
try {
|
||||||
|
channel.open().close();
|
||||||
|
} catch (e if e.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return newURI.spec;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getFile: function getFile(path) {
|
||||||
|
var channel = ios.newChannel(path, null, null);
|
||||||
|
var iStream = channel.open();
|
||||||
|
var siStream = Cc['@mozilla.org/scriptableinputstream;1']
|
||||||
|
.createInstance(Ci.nsIScriptableInputStream);
|
||||||
|
siStream.init(iStream);
|
||||||
|
var data = new String();
|
||||||
|
data += siStream.read(-1);
|
||||||
|
siStream.close();
|
||||||
|
iStream.close();
|
||||||
|
return {contents: data};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (global.window) {
|
||||||
|
// We're being loaded in a chrome window, or a web page with
|
||||||
|
// UniversalXPConnect privileges.
|
||||||
|
global.SecurableModule = exports;
|
||||||
|
} else if (global.exports) {
|
||||||
|
// We're being loaded in a SecurableModule.
|
||||||
|
for (name in exports) {
|
||||||
|
global.exports[name] = exports[name];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We're being loaded in a JS module.
|
||||||
|
global.EXPORTED_SYMBOLS = [];
|
||||||
|
for (name in exports) {
|
||||||
|
global.EXPORTED_SYMBOLS.push(name);
|
||||||
|
global[name] = exports[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(this);
|
|
@ -0,0 +1,88 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Jetpack.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Atul Varma <atul@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
var jsm = {}; Cu.import("resource://gre/modules/XPCOMUtils.jsm", jsm);
|
||||||
|
var XPCOMUtils = jsm.XPCOMUtils;
|
||||||
|
|
||||||
|
var timerClass = Cc["@mozilla.org/timer;1"];
|
||||||
|
var nextID = 1;
|
||||||
|
var timers = {};
|
||||||
|
|
||||||
|
function TimerCallback(callback) {
|
||||||
|
this._callback = callback;
|
||||||
|
this.QueryInterface = XPCOMUtils.generateQI([Ci.nsITimerCallback]);
|
||||||
|
};
|
||||||
|
|
||||||
|
TimerCallback.prototype = {
|
||||||
|
notify : function notify(timer) {
|
||||||
|
try {
|
||||||
|
for (timerID in timers)
|
||||||
|
if (timers[timerID] === timer) {
|
||||||
|
delete timers[timerID];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this._callback.apply(null, []);
|
||||||
|
} catch (e) {
|
||||||
|
console.exception(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var setTimeout = exports.setTimeout = function setTimeout(callback, delay) {
|
||||||
|
var timer = timerClass.createInstance(Ci.nsITimer);
|
||||||
|
|
||||||
|
var timerID = nextID++;
|
||||||
|
timers[timerID] = timer;
|
||||||
|
|
||||||
|
timer.initWithCallback(new TimerCallback(callback),
|
||||||
|
delay,
|
||||||
|
timer.TYPE_ONE_SHOT);
|
||||||
|
return timerID;
|
||||||
|
};
|
||||||
|
|
||||||
|
var clearTimeout = exports.clearTimeout = function clearTimeout(timerID) {
|
||||||
|
var timer = timers[timerID];
|
||||||
|
if (timer) {
|
||||||
|
timer.cancel();
|
||||||
|
delete timers[timerID];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
require("unload").when(
|
||||||
|
function cancelAllPendingTimers() {
|
||||||
|
var timerIDs = [timerID for (timerID in timers)];
|
||||||
|
timerIDs.forEach(function(timerID) { clearTimeout(timerID); });
|
||||||
|
});
|
|
@ -0,0 +1,140 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Jetpack.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Atul Varma <atul@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
// Undo the auto-parentification of URLs done in bug 418356.
|
||||||
|
function deParentifyURL(url) {
|
||||||
|
return url ? url.split(" -> ").slice(-1)[0] : url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: We might want to move this function to url or some similar
|
||||||
|
// module.
|
||||||
|
function getLocalFile(path) {
|
||||||
|
var ios = Cc['@mozilla.org/network/io-service;1']
|
||||||
|
.getService(Ci.nsIIOService);
|
||||||
|
var channel = ios.newChannel(path, null, null);
|
||||||
|
var iStream = channel.open();
|
||||||
|
var siStream = Cc['@mozilla.org/scriptableinputstream;1']
|
||||||
|
.createInstance(Ci.nsIScriptableInputStream);
|
||||||
|
siStream.init(iStream);
|
||||||
|
var data = new String();
|
||||||
|
data += siStream.read(-1);
|
||||||
|
siStream.close();
|
||||||
|
iStream.close();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeGetFileLine(path, line) {
|
||||||
|
try {
|
||||||
|
var scheme = require("url").parse(path).scheme;
|
||||||
|
// TODO: There should be an easier, more accurate way to figure out
|
||||||
|
// what's the case here.
|
||||||
|
if (!(scheme == "http" || scheme == "https"))
|
||||||
|
return getLocalFile(path).split("\n")[line - 1];
|
||||||
|
} catch (e) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorStackToJSON(stack) {
|
||||||
|
var lines = stack.split("\n");
|
||||||
|
|
||||||
|
var frames = [];
|
||||||
|
lines.forEach(
|
||||||
|
function(line) {
|
||||||
|
if (!line)
|
||||||
|
return;
|
||||||
|
var atIndex = line.indexOf("@");
|
||||||
|
var colonIndex = line.lastIndexOf(":");
|
||||||
|
var filename = deParentifyURL(line.slice(atIndex + 1, colonIndex));
|
||||||
|
var lineNo = parseInt(line.slice(colonIndex + 1));
|
||||||
|
var funcSig = line.slice(0, atIndex);
|
||||||
|
var funcName = funcSig.slice(0, funcSig.indexOf("("));
|
||||||
|
frames.unshift({filename: filename,
|
||||||
|
funcName: funcName,
|
||||||
|
lineNo: lineNo});
|
||||||
|
});
|
||||||
|
|
||||||
|
return frames;
|
||||||
|
};
|
||||||
|
|
||||||
|
function nsIStackFramesToJSON(frame) {
|
||||||
|
var stack = [];
|
||||||
|
|
||||||
|
while (frame) {
|
||||||
|
var filename = deParentifyURL(frame.filename);
|
||||||
|
stack.splice(0, 0, {filename: filename,
|
||||||
|
lineNo: frame.lineNumber,
|
||||||
|
funcName: frame.name});
|
||||||
|
frame = frame.caller;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stack;
|
||||||
|
};
|
||||||
|
|
||||||
|
var fromException = exports.fromException = function fromException(e) {
|
||||||
|
if (e instanceof Ci.nsIException)
|
||||||
|
return nsIStackFramesToJSON(e.location);
|
||||||
|
return errorStackToJSON(e.stack);
|
||||||
|
};
|
||||||
|
|
||||||
|
var get = exports.get = function get() {
|
||||||
|
return nsIStackFramesToJSON(Components.stack.caller);
|
||||||
|
};
|
||||||
|
|
||||||
|
var format = exports.format = function format(tbOrException) {
|
||||||
|
if (tbOrException === undefined) {
|
||||||
|
tbOrException = get();
|
||||||
|
tbOrException.splice(-1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tb;
|
||||||
|
if (tbOrException.length === undefined)
|
||||||
|
tb = fromException(tbOrException);
|
||||||
|
else
|
||||||
|
tb = tbOrException;
|
||||||
|
|
||||||
|
var lines = ["Traceback (most recent call last):"];
|
||||||
|
|
||||||
|
tb.forEach(
|
||||||
|
function(frame) {
|
||||||
|
lines.push(' File "' + frame.filename + '", line ' +
|
||||||
|
frame.lineNo + ', in ' + frame.funcName);
|
||||||
|
var sourceLine = safeGetFileLine(frame.filename, frame.lineNo);
|
||||||
|
if (sourceLine)
|
||||||
|
lines.push(' ' + sourceLine.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
};
|
|
@ -0,0 +1,231 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Jetpack.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Atul Varma <atul@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
var timer = require("timer");
|
||||||
|
var file = require("file");
|
||||||
|
|
||||||
|
exports.findAndRunTests = function findAndRunTests(options) {
|
||||||
|
var finder = new TestFinder(options.dirs);
|
||||||
|
var runner = new TestRunner();
|
||||||
|
runner.startMany({tests: finder.findTests(),
|
||||||
|
onDone: options.onDone});
|
||||||
|
};
|
||||||
|
|
||||||
|
var TestFinder = exports.TestFinder = function TestFinder(dirs) {
|
||||||
|
this.dirs = dirs;
|
||||||
|
};
|
||||||
|
|
||||||
|
TestFinder.prototype = {
|
||||||
|
_makeTest: function _makeTest(suite, name, test) {
|
||||||
|
function runTest(runner) {
|
||||||
|
console.info("executing '" + suite + "." + name + "'");
|
||||||
|
test(runner);
|
||||||
|
}
|
||||||
|
return runTest;
|
||||||
|
},
|
||||||
|
|
||||||
|
findTests: function findTests() {
|
||||||
|
var self = this;
|
||||||
|
var tests = [];
|
||||||
|
|
||||||
|
this.dirs.forEach(
|
||||||
|
function(dir) {
|
||||||
|
var suites = [name.slice(0, -3)
|
||||||
|
for each (name in file.list(dir))
|
||||||
|
if (/^test-.*\.js$/.test(name))];
|
||||||
|
|
||||||
|
suites.forEach(
|
||||||
|
function(suite) {
|
||||||
|
var module = require(suite);
|
||||||
|
for (name in module)
|
||||||
|
if (name.indexOf("test") == 0)
|
||||||
|
tests.push(self._makeTest(suite, name, module[name]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return tests;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var TestRunner = exports.TestRunner = function TestRunner(options) {
|
||||||
|
this.passed = 0;
|
||||||
|
this.failed = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
TestRunner.prototype = {
|
||||||
|
DEFAULT_PAUSE_TIMEOUT: 10000,
|
||||||
|
|
||||||
|
makeSandboxedLoader: function makeSandboxedLoader(options) {
|
||||||
|
if (!options)
|
||||||
|
options = {};
|
||||||
|
var Cuddlefish = require("cuddlefish");
|
||||||
|
|
||||||
|
options.fs = Cuddlefish.parentLoader.fs;
|
||||||
|
return new Cuddlefish.Loader(options);
|
||||||
|
},
|
||||||
|
|
||||||
|
pass: function pass(message) {
|
||||||
|
console.info("pass:", message);
|
||||||
|
this.passed++;
|
||||||
|
},
|
||||||
|
|
||||||
|
fail: function fail(message) {
|
||||||
|
console.error("fail:", message);
|
||||||
|
this.failed++;
|
||||||
|
},
|
||||||
|
|
||||||
|
exception: function exception(e) {
|
||||||
|
console.exception(e);
|
||||||
|
this.failed++;
|
||||||
|
},
|
||||||
|
|
||||||
|
assertMatches: function assertMatches(string, regexp, message) {
|
||||||
|
if (regexp.test(string)) {
|
||||||
|
if (!message)
|
||||||
|
message = uneval(string) + " matches " + uneval(regexp);
|
||||||
|
this.pass(message);
|
||||||
|
} else {
|
||||||
|
var no = uneval(string) + " doesn't match " + uneval(regexp);
|
||||||
|
if (!message)
|
||||||
|
message = no;
|
||||||
|
else
|
||||||
|
message = message + " (" + no + ")";
|
||||||
|
this.fail(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
assertRaises: function assertRaises(func, predicate, message) {
|
||||||
|
try {
|
||||||
|
func();
|
||||||
|
if (message)
|
||||||
|
this.fail(message + " (no exception thrown)");
|
||||||
|
else
|
||||||
|
this.fail("function failed to throw exception");
|
||||||
|
} catch (e) {
|
||||||
|
if (typeof(predicate) == "object")
|
||||||
|
this.assertMatches(e.message, predicate, message);
|
||||||
|
else
|
||||||
|
this.assertEqual(e.message, predicate, message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
assertNotEqual: function assertNotEqual(a, b, message) {
|
||||||
|
if (a != b) {
|
||||||
|
if (!message)
|
||||||
|
message = "a != b != " + uneval(a);
|
||||||
|
this.pass(message);
|
||||||
|
} else {
|
||||||
|
var equality = uneval(a) + " == " + uneval(b);
|
||||||
|
if (!message)
|
||||||
|
message = equality;
|
||||||
|
else
|
||||||
|
message += " (" + equality + ")";
|
||||||
|
this.fail(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
assertEqual: function assertEqual(a, b, message) {
|
||||||
|
if (a == b) {
|
||||||
|
if (!message)
|
||||||
|
message = "a == b == " + uneval(a);
|
||||||
|
this.pass(message);
|
||||||
|
} else {
|
||||||
|
var inequality = uneval(a) + " != " + uneval(b);
|
||||||
|
if (!message)
|
||||||
|
message = inequality;
|
||||||
|
else
|
||||||
|
message += " (" + inequality + ")";
|
||||||
|
this.fail(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
done: function done() {
|
||||||
|
if (!this.isDone) {
|
||||||
|
this.isDone = true;
|
||||||
|
if (this.waitTimeout !== null) {
|
||||||
|
timer.clearTimeout(this.waitTimeout);
|
||||||
|
this.waitTimeout = null;
|
||||||
|
}
|
||||||
|
if (this.onDone !== null) {
|
||||||
|
var onDone = this.onDone;
|
||||||
|
this.onDone = null;
|
||||||
|
onDone(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
waitUntilDone: function waitUntilDone(ms) {
|
||||||
|
if (ms === undefined)
|
||||||
|
ms = this.DEFAULT_PAUSE_TIMEOUT;
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
function tiredOfWaiting() {
|
||||||
|
self.failed++;
|
||||||
|
self.done();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.waitTimeout = timer.setTimeout(tiredOfWaiting, ms);
|
||||||
|
},
|
||||||
|
|
||||||
|
startMany: function startMany(options) {
|
||||||
|
function scheduleNextTest(self) {
|
||||||
|
function runNextTest() {
|
||||||
|
var test = options.tests.pop();
|
||||||
|
if (test)
|
||||||
|
self.start({test: test, onDone: scheduleNextTest});
|
||||||
|
else
|
||||||
|
options.onDone(self);
|
||||||
|
}
|
||||||
|
timer.setTimeout(runNextTest, 0);
|
||||||
|
}
|
||||||
|
scheduleNextTest(this);
|
||||||
|
},
|
||||||
|
|
||||||
|
start: function start(options) {
|
||||||
|
this.test = options.test;
|
||||||
|
this.isDone = false;
|
||||||
|
this.onDone = options.onDone;
|
||||||
|
this.waitTimeout = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.test(this);
|
||||||
|
} catch (e) {
|
||||||
|
this.exception(e);
|
||||||
|
}
|
||||||
|
if (this.waitTimeout === null)
|
||||||
|
this.done();
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
// This module was taken from narwhal:
|
||||||
|
//
|
||||||
|
// http://narwhaljs.org
|
||||||
|
|
||||||
|
var observers = [];
|
||||||
|
|
||||||
|
exports.when = function (observer) {
|
||||||
|
observers.unshift(observer);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.send = function () {
|
||||||
|
observers.forEach(function (observer) {
|
||||||
|
observer();
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,110 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Jetpack.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Atul Varma <atul@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
var ios = Cc['@mozilla.org/network/io-service;1']
|
||||||
|
.getService(Ci.nsIIOService);
|
||||||
|
|
||||||
|
var resProt = ios.getProtocolHandler("resource")
|
||||||
|
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||||
|
|
||||||
|
function newURI(uriStr) {
|
||||||
|
try {
|
||||||
|
return ios.newURI(uriStr, null, null);
|
||||||
|
} catch (e if e.result == Cr.NS_ERROR_MALFORMED_URI) {
|
||||||
|
throw new Error("malformed URI: " + uriStr);
|
||||||
|
} catch (e if (e.result == Cr.NS_ERROR_FAILURE ||
|
||||||
|
e.result == Cr.NS_ERROR_ILLEGAL_VALUE)) {
|
||||||
|
throw new Error("invalid URI: " + uriStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveResourceURI(uri) {
|
||||||
|
var resolved;
|
||||||
|
try {
|
||||||
|
resolved = resProt.resolveURI(uri);
|
||||||
|
} catch (e if e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
|
||||||
|
throw new Error("resource does not exist: " + uri.spec);
|
||||||
|
};
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fromFilename = exports.fromFilename = function fromFilename(path) {
|
||||||
|
var file = Cc['@mozilla.org/file/local;1']
|
||||||
|
.createInstance(Ci.nsILocalFile);
|
||||||
|
file.initWithPath(path);
|
||||||
|
return ios.newFileURI(file).spec;
|
||||||
|
};
|
||||||
|
|
||||||
|
var toFilename = exports.toFilename = function toFilename(url) {
|
||||||
|
var uri = newURI(url);
|
||||||
|
if (uri.scheme == "resource")
|
||||||
|
uri = newURI(resolveResourceURI(uri));
|
||||||
|
if (uri.scheme == "file") {
|
||||||
|
var file = uri.QueryInterface(Ci.nsIFileURL).file;
|
||||||
|
return file.path;
|
||||||
|
}
|
||||||
|
throw new Error("cannot map to filename: " + url);
|
||||||
|
};
|
||||||
|
|
||||||
|
var parse = exports.parse = function parse(url) {
|
||||||
|
var uri = newURI(url);
|
||||||
|
|
||||||
|
var userPass = null;
|
||||||
|
try {
|
||||||
|
userPass = uri.userPass ? uri.userPass : null;
|
||||||
|
} catch (e if e.result == Cr.NS_ERROR_FAILURE) {}
|
||||||
|
|
||||||
|
var host = null;
|
||||||
|
try {
|
||||||
|
host = uri.host;
|
||||||
|
} catch (e if e.result == Cr.NS_ERROR_FAILURE) {}
|
||||||
|
|
||||||
|
var port = null;
|
||||||
|
try {
|
||||||
|
port = uri.port == -1 ? null : uri.port;
|
||||||
|
} catch (e if e.result == Cr.NS_ERROR_FAILURE) {}
|
||||||
|
|
||||||
|
return {scheme: uri.scheme,
|
||||||
|
userPass: userPass,
|
||||||
|
host: host,
|
||||||
|
port: port,
|
||||||
|
path: uri.path};
|
||||||
|
};
|
||||||
|
|
||||||
|
var resolve = exports.resolve = function resolve(base, relative) {
|
||||||
|
var baseURI = newURI(base);
|
||||||
|
return ios.newURI(relative, null, baseURI).spec;
|
||||||
|
};
|
|
@ -0,0 +1,565 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is log4moz
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is
|
||||||
|
* Michael Johnston
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2006
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Michael Johnston <special.michael@gmail.com>
|
||||||
|
* Dan Mills <thunder@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
const EXPORTED_SYMBOLS = ['Log4Moz'];
|
||||||
|
|
||||||
|
const Cc = Components.classes;
|
||||||
|
const Ci = Components.interfaces;
|
||||||
|
const Cr = Components.results;
|
||||||
|
const Cu = Components.utils;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
|
||||||
|
const MODE_RDONLY = 0x01;
|
||||||
|
const MODE_WRONLY = 0x02;
|
||||||
|
const MODE_CREATE = 0x08;
|
||||||
|
const MODE_APPEND = 0x10;
|
||||||
|
const MODE_TRUNCATE = 0x20;
|
||||||
|
|
||||||
|
const PERMS_FILE = 0644;
|
||||||
|
const PERMS_DIRECTORY = 0755;
|
||||||
|
|
||||||
|
const ONE_BYTE = 1;
|
||||||
|
const ONE_KILOBYTE = 1024 * ONE_BYTE;
|
||||||
|
const ONE_MEGABYTE = 1024 * ONE_KILOBYTE;
|
||||||
|
|
||||||
|
let Log4Moz = {
|
||||||
|
Level: {
|
||||||
|
Fatal: 70,
|
||||||
|
Error: 60,
|
||||||
|
Warn: 50,
|
||||||
|
Info: 40,
|
||||||
|
Config: 30,
|
||||||
|
Debug: 20,
|
||||||
|
Trace: 10,
|
||||||
|
All: 0,
|
||||||
|
Desc: {
|
||||||
|
70: "FATAL",
|
||||||
|
60: "ERROR",
|
||||||
|
50: "WARN",
|
||||||
|
40: "INFO",
|
||||||
|
30: "CONFIG",
|
||||||
|
20: "DEBUG",
|
||||||
|
10: "TRACE",
|
||||||
|
0: "ALL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get repository() {
|
||||||
|
delete Log4Moz.repository;
|
||||||
|
Log4Moz.repository = new LoggerRepository();
|
||||||
|
return Log4Moz.repository;
|
||||||
|
},
|
||||||
|
set repository(value) {
|
||||||
|
delete Log4Moz.repository;
|
||||||
|
Log4Moz.repository = value;
|
||||||
|
},
|
||||||
|
|
||||||
|
get LogMessage() { return LogMessage; },
|
||||||
|
get Logger() { return Logger; },
|
||||||
|
get LoggerRepository() { return LoggerRepository; },
|
||||||
|
|
||||||
|
get Formatter() { return Formatter; },
|
||||||
|
get BasicFormatter() { return BasicFormatter; },
|
||||||
|
|
||||||
|
get Appender() { return Appender; },
|
||||||
|
get DumpAppender() { return DumpAppender; },
|
||||||
|
get ConsoleAppender() { return ConsoleAppender; },
|
||||||
|
get FileAppender() { return FileAppender; },
|
||||||
|
get RotatingFileAppender() { return RotatingFileAppender; },
|
||||||
|
|
||||||
|
// Logging helper:
|
||||||
|
// let logger = Log4Moz.repository.getLogger("foo");
|
||||||
|
// logger.info(Log4Moz.enumerateInterfaces(someObject).join(","));
|
||||||
|
enumerateInterfaces: function Log4Moz_enumerateInterfaces(aObject) {
|
||||||
|
let interfaces = [];
|
||||||
|
|
||||||
|
for (i in Ci) {
|
||||||
|
try {
|
||||||
|
aObject.QueryInterface(Ci[i]);
|
||||||
|
interfaces.push(i);
|
||||||
|
}
|
||||||
|
catch(ex) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaces;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Logging helper:
|
||||||
|
// let logger = Log4Moz.repository.getLogger("foo");
|
||||||
|
// logger.info(Log4Moz.enumerateProperties(someObject).join(","));
|
||||||
|
enumerateProperties: function Log4Moz_enumerateProps(aObject,
|
||||||
|
aExcludeComplexTypes) {
|
||||||
|
let properties = [];
|
||||||
|
|
||||||
|
for (p in aObject) {
|
||||||
|
try {
|
||||||
|
if (aExcludeComplexTypes &&
|
||||||
|
(typeof aObject[p] == "object" || typeof aObject[p] == "function"))
|
||||||
|
continue;
|
||||||
|
properties.push(p + " = " + aObject[p]);
|
||||||
|
}
|
||||||
|
catch(ex) {
|
||||||
|
properties.push(p + " = " + ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* LogMessage
|
||||||
|
* Encapsulates a single log event's data
|
||||||
|
*/
|
||||||
|
function LogMessage(loggerName, level, message){
|
||||||
|
this.loggerName = loggerName;
|
||||||
|
this.message = message;
|
||||||
|
this.level = level;
|
||||||
|
this.time = Date.now();
|
||||||
|
}
|
||||||
|
LogMessage.prototype = {
|
||||||
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
|
||||||
|
|
||||||
|
get levelDesc() {
|
||||||
|
if (this.level in Log4Moz.Level.Desc)
|
||||||
|
return Log4Moz.Level.Desc[this.level];
|
||||||
|
return "UNKNOWN";
|
||||||
|
},
|
||||||
|
|
||||||
|
toString: function LogMsg_toString(){
|
||||||
|
return "LogMessage [" + this.time + " " + this.level + " " +
|
||||||
|
this.message + "]";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Logger
|
||||||
|
* Hierarchical version. Logs to all appenders, assigned or inherited
|
||||||
|
*/
|
||||||
|
|
||||||
|
function Logger(name, repository) {
|
||||||
|
this._init(name, repository);
|
||||||
|
}
|
||||||
|
Logger.prototype = {
|
||||||
|
_init: function Logger__init(name, repository) {
|
||||||
|
if (!repository)
|
||||||
|
repository = Log4Moz.repository;
|
||||||
|
this._name = name;
|
||||||
|
this._appenders = [];
|
||||||
|
this._repository = repository;
|
||||||
|
},
|
||||||
|
|
||||||
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
|
||||||
|
|
||||||
|
parent: null,
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this._name;
|
||||||
|
},
|
||||||
|
|
||||||
|
_level: null,
|
||||||
|
get level() {
|
||||||
|
if (this._level != null)
|
||||||
|
return this._level;
|
||||||
|
if (this.parent)
|
||||||
|
return this.parent.level;
|
||||||
|
dump("log4moz warning: root logger configuration error: no level defined\n");
|
||||||
|
return Log4Moz.Level.All;
|
||||||
|
},
|
||||||
|
set level(level) {
|
||||||
|
this._level = level;
|
||||||
|
},
|
||||||
|
|
||||||
|
_appenders: null,
|
||||||
|
get appenders() {
|
||||||
|
if (!this.parent)
|
||||||
|
return this._appenders;
|
||||||
|
return this._appenders.concat(this.parent.appenders);
|
||||||
|
},
|
||||||
|
|
||||||
|
addAppender: function Logger_addAppender(appender) {
|
||||||
|
for (let i = 0; i < this._appenders.length; i++) {
|
||||||
|
if (this._appenders[i] == appender)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._appenders.push(appender);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAppender: function Logger_removeAppender(appender) {
|
||||||
|
let newAppenders = [];
|
||||||
|
for (let i = 0; i < this._appenders.length; i++) {
|
||||||
|
if (this._appenders[i] != appender)
|
||||||
|
newAppenders.push(this._appenders[i]);
|
||||||
|
}
|
||||||
|
this._appenders = newAppenders;
|
||||||
|
},
|
||||||
|
|
||||||
|
log: function Logger_log(message) {
|
||||||
|
if (this.level > message.level)
|
||||||
|
return;
|
||||||
|
let appenders = this.appenders;
|
||||||
|
for (let i = 0; i < appenders.length; i++){
|
||||||
|
appenders[i].append(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fatal: function Logger_fatal(string) {
|
||||||
|
this.log(new LogMessage(this._name, Log4Moz.Level.Fatal, string));
|
||||||
|
},
|
||||||
|
error: function Logger_error(string) {
|
||||||
|
this.log(new LogMessage(this._name, Log4Moz.Level.Error, string));
|
||||||
|
},
|
||||||
|
warn: function Logger_warn(string) {
|
||||||
|
this.log(new LogMessage(this._name, Log4Moz.Level.Warn, string));
|
||||||
|
},
|
||||||
|
info: function Logger_info(string) {
|
||||||
|
this.log(new LogMessage(this._name, Log4Moz.Level.Info, string));
|
||||||
|
},
|
||||||
|
config: function Logger_config(string) {
|
||||||
|
this.log(new LogMessage(this._name, Log4Moz.Level.Config, string));
|
||||||
|
},
|
||||||
|
debug: function Logger_debug(string) {
|
||||||
|
this.log(new LogMessage(this._name, Log4Moz.Level.Debug, string));
|
||||||
|
},
|
||||||
|
trace: function Logger_trace(string) {
|
||||||
|
this.log(new LogMessage(this._name, Log4Moz.Level.Trace, string));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* LoggerRepository
|
||||||
|
* Implements a hierarchy of Loggers
|
||||||
|
*/
|
||||||
|
|
||||||
|
function LoggerRepository() {}
|
||||||
|
LoggerRepository.prototype = {
|
||||||
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
|
||||||
|
|
||||||
|
_loggers: {},
|
||||||
|
|
||||||
|
_rootLogger: null,
|
||||||
|
get rootLogger() {
|
||||||
|
if (!this._rootLogger) {
|
||||||
|
this._rootLogger = new Logger("root", this);
|
||||||
|
this._rootLogger.level = Log4Moz.Level.All;
|
||||||
|
}
|
||||||
|
return this._rootLogger;
|
||||||
|
},
|
||||||
|
// FIXME: need to update all parent values if we do this
|
||||||
|
//set rootLogger(logger) {
|
||||||
|
// this._rootLogger = logger;
|
||||||
|
//},
|
||||||
|
|
||||||
|
_updateParents: function LogRep__updateParents(name) {
|
||||||
|
let pieces = name.split('.');
|
||||||
|
let cur, parent;
|
||||||
|
|
||||||
|
// find the closest parent
|
||||||
|
// don't test for the logger name itself, as there's a chance it's already
|
||||||
|
// there in this._loggers
|
||||||
|
for (let i = 0; i < pieces.length - 1; i++) {
|
||||||
|
if (cur)
|
||||||
|
cur += '.' + pieces[i];
|
||||||
|
else
|
||||||
|
cur = pieces[i];
|
||||||
|
if (cur in this._loggers)
|
||||||
|
parent = cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we didn't assign a parent above, there is no parent
|
||||||
|
if (!parent)
|
||||||
|
this._loggers[name].parent = this.rootLogger;
|
||||||
|
else
|
||||||
|
this._loggers[name].parent = this._loggers[parent];
|
||||||
|
|
||||||
|
// trigger updates for any possible descendants of this logger
|
||||||
|
for (let logger in this._loggers) {
|
||||||
|
if (logger != name && logger.indexOf(name) == 0)
|
||||||
|
this._updateParents(logger);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getLogger: function LogRep_getLogger(name) {
|
||||||
|
if (!name)
|
||||||
|
name = this.getLogger.caller.name;
|
||||||
|
if (name in this._loggers)
|
||||||
|
return this._loggers[name];
|
||||||
|
this._loggers[name] = new Logger(name, this);
|
||||||
|
this._updateParents(name);
|
||||||
|
return this._loggers[name];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Formatters
|
||||||
|
* These massage a LogMessage into whatever output is desired
|
||||||
|
* Only the BasicFormatter is currently implemented
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Abstract formatter
|
||||||
|
function Formatter() {}
|
||||||
|
Formatter.prototype = {
|
||||||
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
|
||||||
|
format: function Formatter_format(message) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// FIXME: should allow for formatting the whole string, not just the date
|
||||||
|
function BasicFormatter(dateFormat) {
|
||||||
|
if (dateFormat)
|
||||||
|
this.dateFormat = dateFormat;
|
||||||
|
}
|
||||||
|
BasicFormatter.prototype = {
|
||||||
|
__proto__: Formatter.prototype,
|
||||||
|
|
||||||
|
_dateFormat: null,
|
||||||
|
|
||||||
|
get dateFormat() {
|
||||||
|
if (!this._dateFormat)
|
||||||
|
this._dateFormat = "%Y-%m-%d %H:%M:%S";
|
||||||
|
return this._dateFormat;
|
||||||
|
},
|
||||||
|
|
||||||
|
set dateFormat(format) {
|
||||||
|
this._dateFormat = format;
|
||||||
|
},
|
||||||
|
|
||||||
|
format: function BF_format(message) {
|
||||||
|
// Pad a string to a certain length (20) with a character (space)
|
||||||
|
let pad = function BF__pad(str, len, chr) str +
|
||||||
|
new Array(Math.max((len || 20) - str.length + 1, 0)).join(chr || " ");
|
||||||
|
|
||||||
|
// Generate a date string because toLocaleString doesn't work XXX 514803
|
||||||
|
let z = function(n) n < 10 ? "0" + n : n;
|
||||||
|
let d = new Date(message.time);
|
||||||
|
let dateStr = [d.getFullYear(), "-", z(d.getMonth() + 1), "-",
|
||||||
|
z(d.getDate()), " ", z(d.getHours()), ":", z(d.getMinutes()), ":",
|
||||||
|
z(d.getSeconds())].join("");
|
||||||
|
|
||||||
|
return dateStr + "\t" + pad(message.loggerName) + " " + message.levelDesc +
|
||||||
|
"\t" + message.message + "\n";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Appenders
|
||||||
|
* These can be attached to Loggers to log to different places
|
||||||
|
* Simply subclass and override doAppend to implement a new one
|
||||||
|
*/
|
||||||
|
|
||||||
|
function Appender(formatter) {
|
||||||
|
this._name = "Appender";
|
||||||
|
this._formatter = formatter? formatter : new BasicFormatter();
|
||||||
|
}
|
||||||
|
Appender.prototype = {
|
||||||
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
|
||||||
|
|
||||||
|
_level: Log4Moz.Level.All,
|
||||||
|
get level() { return this._level; },
|
||||||
|
set level(level) { this._level = level; },
|
||||||
|
|
||||||
|
append: function App_append(message) {
|
||||||
|
if(this._level <= message.level)
|
||||||
|
this.doAppend(this._formatter.format(message));
|
||||||
|
},
|
||||||
|
toString: function App_toString() {
|
||||||
|
return this._name + " [level=" + this._level +
|
||||||
|
", formatter=" + this._formatter + "]";
|
||||||
|
},
|
||||||
|
doAppend: function App_doAppend(message) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* DumpAppender
|
||||||
|
* Logs to standard out
|
||||||
|
*/
|
||||||
|
|
||||||
|
function DumpAppender(formatter) {
|
||||||
|
this._name = "DumpAppender";
|
||||||
|
this._formatter = formatter? formatter : new BasicFormatter();
|
||||||
|
}
|
||||||
|
DumpAppender.prototype = {
|
||||||
|
__proto__: Appender.prototype,
|
||||||
|
|
||||||
|
doAppend: function DApp_doAppend(message) {
|
||||||
|
dump(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ConsoleAppender
|
||||||
|
* Logs to the javascript console
|
||||||
|
*/
|
||||||
|
|
||||||
|
function ConsoleAppender(formatter) {
|
||||||
|
this._name = "ConsoleAppender";
|
||||||
|
this._formatter = formatter;
|
||||||
|
}
|
||||||
|
ConsoleAppender.prototype = {
|
||||||
|
__proto__: Appender.prototype,
|
||||||
|
|
||||||
|
doAppend: function CApp_doAppend(message) {
|
||||||
|
if (message.level > Log4Moz.Level.Warn) {
|
||||||
|
Cu.reportError(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Cc["@mozilla.org/consoleservice;1"].
|
||||||
|
getService(Ci.nsIConsoleService).logStringMessage(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FileAppender
|
||||||
|
* Logs to a file
|
||||||
|
*/
|
||||||
|
|
||||||
|
function FileAppender(file, formatter) {
|
||||||
|
this._name = "FileAppender";
|
||||||
|
this._file = file; // nsIFile
|
||||||
|
this._formatter = formatter? formatter : new BasicFormatter();
|
||||||
|
}
|
||||||
|
FileAppender.prototype = {
|
||||||
|
__proto__: Appender.prototype,
|
||||||
|
__fos: null,
|
||||||
|
get _fos() {
|
||||||
|
if (!this.__fos)
|
||||||
|
this.openStream();
|
||||||
|
return this.__fos;
|
||||||
|
},
|
||||||
|
|
||||||
|
openStream: function FApp_openStream() {
|
||||||
|
try {
|
||||||
|
let __fos = Cc["@mozilla.org/network/file-output-stream;1"].
|
||||||
|
createInstance(Ci.nsIFileOutputStream);
|
||||||
|
let flags = MODE_WRONLY | MODE_CREATE | MODE_APPEND;
|
||||||
|
__fos.init(this._file, flags, PERMS_FILE, 0);
|
||||||
|
|
||||||
|
this.__fos = Cc["@mozilla.org/intl/converter-output-stream;1"]
|
||||||
|
.createInstance(Ci.nsIConverterOutputStream);
|
||||||
|
this.__fos.init(__fos, "UTF-8", 4096,
|
||||||
|
Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
|
||||||
|
} catch(e) {
|
||||||
|
dump("Error opening stream:\n" + e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeStream: function FApp_closeStream() {
|
||||||
|
if (!this.__fos)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
this.__fos.close();
|
||||||
|
this.__fos = null;
|
||||||
|
} catch(e) {
|
||||||
|
dump("Failed to close file output stream\n" + e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
doAppend: function FApp_doAppend(message) {
|
||||||
|
if (message === null || message.length <= 0)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
this._fos.writeString(message);
|
||||||
|
} catch(e) {
|
||||||
|
dump("Error writing file:\n" + e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: function FApp_clear() {
|
||||||
|
this.closeStream();
|
||||||
|
try {
|
||||||
|
this._file.remove(false);
|
||||||
|
} catch (e) {
|
||||||
|
// XXX do something?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* RotatingFileAppender
|
||||||
|
* Similar to FileAppender, but rotates logs when they become too large
|
||||||
|
*/
|
||||||
|
|
||||||
|
function RotatingFileAppender(file, formatter, maxSize, maxBackups) {
|
||||||
|
if (maxSize === undefined)
|
||||||
|
maxSize = ONE_MEGABYTE * 2;
|
||||||
|
|
||||||
|
if (maxBackups === undefined)
|
||||||
|
maxBackups = 0;
|
||||||
|
|
||||||
|
this._name = "RotatingFileAppender";
|
||||||
|
this._file = file; // nsIFile
|
||||||
|
this._formatter = formatter? formatter : new BasicFormatter();
|
||||||
|
this._maxSize = maxSize;
|
||||||
|
this._maxBackups = maxBackups;
|
||||||
|
}
|
||||||
|
RotatingFileAppender.prototype = {
|
||||||
|
__proto__: FileAppender.prototype,
|
||||||
|
|
||||||
|
doAppend: function RFApp_doAppend(message) {
|
||||||
|
if (message === null || message.length <= 0)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
this.rotateLogs();
|
||||||
|
FileAppender.prototype.doAppend.call(this, message);
|
||||||
|
} catch(e) {
|
||||||
|
dump("Error writing file:" + e + "\n");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
rotateLogs: function RFApp_rotateLogs() {
|
||||||
|
if(this._file.exists() &&
|
||||||
|
this._file.fileSize < this._maxSize)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.closeStream();
|
||||||
|
|
||||||
|
for (let i = this.maxBackups - 1; i > 0; i--){
|
||||||
|
let backup = this._file.parent.clone();
|
||||||
|
backup.append(this._file.leafName + "." + i);
|
||||||
|
if (backup.exists())
|
||||||
|
backup.moveTo(this._file.parent, this._file.leafName + "." + (i + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cur = this._file.clone();
|
||||||
|
if (cur.exists())
|
||||||
|
cur.moveTo(cur.parent, cur.leafName + ".1");
|
||||||
|
|
||||||
|
// Note: this._file still points to the same file
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,175 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Test Pilot.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
* Dan Mills <thunder@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
EXPORTED_SYMBOLS = ["MetadataCollector"];
|
||||||
|
|
||||||
|
const Cc = Components.classes;
|
||||||
|
const Ci = Components.interfaces;
|
||||||
|
const Cu = Components.utils;
|
||||||
|
|
||||||
|
Cu.import("resource://testpilot/modules/string_sanitizer.js");
|
||||||
|
|
||||||
|
const LOCALE_PREF = "general.useragent.locale";
|
||||||
|
const EXTENSION_ID = "testpilot@labs.mozilla.com";
|
||||||
|
const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
|
||||||
|
const PREFIX_ITEM_URI = "urn:mozilla:item:";
|
||||||
|
|
||||||
|
/* The following preference, if present, stores answers to the basic panel
|
||||||
|
* survey, which tell us user's general tech level, and so should be included
|
||||||
|
* with any upload.*/
|
||||||
|
const SURVEY_ANS = "extensions.testpilot.surveyAnswers.basic_panel_survey_2";
|
||||||
|
|
||||||
|
let Application = Cc["@mozilla.org/fuel/application;1"]
|
||||||
|
.getService(Ci.fuelIApplication);
|
||||||
|
|
||||||
|
// This function copied over from Weave:
|
||||||
|
function Weave_sha1(string) {
|
||||||
|
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
|
||||||
|
createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||||
|
converter.charset = "UTF-8";
|
||||||
|
|
||||||
|
let hasher = Cc["@mozilla.org/security/hash;1"]
|
||||||
|
.createInstance(Ci.nsICryptoHash);
|
||||||
|
hasher.init(hasher.SHA1);
|
||||||
|
|
||||||
|
let data = converter.convertToByteArray(string, {});
|
||||||
|
hasher.update(data, data.length);
|
||||||
|
let rawHash = hasher.finish(false);
|
||||||
|
|
||||||
|
// return the two-digit hexadecimal code for a byte
|
||||||
|
function toHexString(charCode) {
|
||||||
|
return ("0" + charCode.toString(16)).slice(-2);
|
||||||
|
}
|
||||||
|
let hash = [toHexString(rawHash.charCodeAt(i)) for (i in rawHash)].join("");
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
let MetadataCollector = {
|
||||||
|
// Collects metadata such as what country you're in, what extensions you have installed, etc.
|
||||||
|
getExtensions: function MetadataCollector_getExtensions(callback) {
|
||||||
|
//http://lxr.mozilla.org/aviarybranch/source/toolkit/mozapps/extensions/public/nsIExtensionManager.idl
|
||||||
|
//http://lxr.mozilla.org/aviarybranch/source/toolkit/mozapps/update/public/nsIUpdateService.idl#45
|
||||||
|
let myExtensions = [];
|
||||||
|
if (Application.extensions) {
|
||||||
|
for each (let ex in Application.extensions.all) {
|
||||||
|
myExtensions.push({ id: Weave_sha1(ex.id), isEnabled: ex.enabled });
|
||||||
|
}
|
||||||
|
callback(myExtensions);
|
||||||
|
} else {
|
||||||
|
Application.getExtensions(function(extensions) {
|
||||||
|
for each (let ex in extensions.all) {
|
||||||
|
myExtensions.push({ id: Weave_sha1(ex.id), isEnabled: ex.enabled });
|
||||||
|
}
|
||||||
|
callback(myExtensions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getAccessibilities : function MetadataCollector_getAccessibilities() {
|
||||||
|
let prefs =
|
||||||
|
Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService);
|
||||||
|
let branch = prefs.getBranch("accessibility.");
|
||||||
|
let accessibilities = [];
|
||||||
|
let children = branch.getChildList("", {});
|
||||||
|
let length = children.length;
|
||||||
|
let prefName;
|
||||||
|
let prefValue;
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
prefName = "accessibility." + children[i];
|
||||||
|
prefValue =
|
||||||
|
Application.prefs.getValue(prefName, "");
|
||||||
|
accessibilities.push({ name: prefName, value: prefValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessibilities;
|
||||||
|
},
|
||||||
|
|
||||||
|
getLocation: function MetadataCollector_getLocation() {
|
||||||
|
//navitagor.geolocation; // or nsIDOMGeoGeolocation
|
||||||
|
// we don't want the lat/long, we just want the country
|
||||||
|
|
||||||
|
return Application.prefs.getValue(LOCALE_PREF, "");
|
||||||
|
},
|
||||||
|
|
||||||
|
getVersion: function MetadataCollector_getVersion() {
|
||||||
|
return Application.version;
|
||||||
|
},
|
||||||
|
|
||||||
|
getOperatingSystem: function MetadataCollector_getOSVersion() {
|
||||||
|
let oscpu = Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu;
|
||||||
|
let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
|
||||||
|
return os + " " + oscpu;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSurveyAnswers: function MetadataCollector_getSurveyAnswers() {
|
||||||
|
let answers = Application.prefs.getValue(SURVEY_ANS, "");
|
||||||
|
if (answers == "") {
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return sanitizeJSONStrings( JSON.parse(answers) );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getTestPilotVersion: function MetadataCollector_getTPVersion(callback) {
|
||||||
|
// Application.extensions is undefined if we're in Firefox 4.
|
||||||
|
if (Application.extensions) {
|
||||||
|
callback(Application.extensions.get(EXTENSION_ID).version);
|
||||||
|
} else {
|
||||||
|
Application.getExtensions(function(extensions) {
|
||||||
|
callback(extensions.get(EXTENSION_ID).version);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getMetadata: function MetadataCollector_getMetadata(callback) {
|
||||||
|
let self = this;
|
||||||
|
self.getTestPilotVersion(function(tpVersion) {
|
||||||
|
self.getExtensions(function(extensions) {
|
||||||
|
callback({ extensions: extensions,
|
||||||
|
accessibilities: self.getAccessibilities(),
|
||||||
|
location: self.getLocation(),
|
||||||
|
fxVersion: self.getVersion(),
|
||||||
|
operatingSystem: self.getOperatingSystem(),
|
||||||
|
tpVersion: tpVersion,
|
||||||
|
surveyAnswers: self.getSurveyAnswers()}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// TODO if we make a GUID for the user, we keep it here.
|
||||||
|
};
|
|
@ -0,0 +1,339 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Test Pilot.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
const BASE_URL_PREF = "extensions.testpilot.indexBaseURL";
|
||||||
|
var Cuddlefish = require("cuddlefish");
|
||||||
|
var resolveUrl = require("url").resolve;
|
||||||
|
var SecurableModule = require("securable-module");
|
||||||
|
let JarStore = require("jar-code-store").JarStore;
|
||||||
|
|
||||||
|
/* Security info should look like this:
|
||||||
|
* Security Info:
|
||||||
|
Security state: secure
|
||||||
|
Security description: Authenticated by Equifax
|
||||||
|
Security error message: null
|
||||||
|
|
||||||
|
Certificate Status:
|
||||||
|
Verification: OK
|
||||||
|
Common name (CN) = *.mozillalabs.com
|
||||||
|
Organisation = Mozilla Corporation
|
||||||
|
Issuer = Equifax
|
||||||
|
SHA1 fingerprint = E5:CD:91:97:08:E6:88:F2:A2:AE:31:3C:F9:91:8D:14:33:07:C4:EE
|
||||||
|
Valid from 8/12/09 14:04:39
|
||||||
|
Valid until 8/14/11 3:27:26
|
||||||
|
*/
|
||||||
|
|
||||||
|
function verifyChannelSecurity(channel) {
|
||||||
|
// http://mdn.beonex.com/En/How_to_check_the_security_state_of_an_XMLHTTPRequest_over_SSL
|
||||||
|
// Expect channel to have security state = secure, CN = *.mozillalabs.com,
|
||||||
|
// Organization = "Mozilla Corporation", verification = OK.
|
||||||
|
console.info("Verifying SSL channel security info before download...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (! channel instanceof Ci.nsIChannel) {
|
||||||
|
console.warn("Not a channel. This should never happen.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let secInfo = channel.securityInfo;
|
||||||
|
|
||||||
|
if (secInfo instanceof Ci.nsITransportSecurityInfo) {
|
||||||
|
secInfo.QueryInterface(Ci.nsITransportSecurityInfo);
|
||||||
|
let secState = secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_SECURE;
|
||||||
|
if (secState != Ci.nsIWebProgressListener.STATE_IS_SECURE) {
|
||||||
|
console.warn("Failing security check: Security state is not secure.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Failing secuity check: No TransportSecurityInfo.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check SSL certificate details
|
||||||
|
if (secInfo instanceof Ci.nsISSLStatusProvider) {
|
||||||
|
let cert = secInfo.QueryInterface(Ci.nsISSLStatusProvider).
|
||||||
|
SSLStatus.QueryInterface(Ci.nsISSLStatus).serverCert;
|
||||||
|
|
||||||
|
let verificationResult = cert.verifyForUsage(
|
||||||
|
Ci.nsIX509Cert.CERT_USAGE_SSLServer);
|
||||||
|
if (verificationResult != Ci.nsIX509Cert.VERIFIED_OK) {
|
||||||
|
console.warn("Failing security check: Cert not verified OK.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (cert.commonName != "*.mozillalabs.com") {
|
||||||
|
console.warn("Failing security check: Cert not for *.mozillalabs.com");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (cert.organization != "Mozilla Corporation") {
|
||||||
|
console.warn("Failing security check: Cert not for Mozilla corporation.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Failing security check: No SSL cert info.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passed everything
|
||||||
|
console.info("Channel passed SSL security check.");
|
||||||
|
return true;
|
||||||
|
} catch(err) {
|
||||||
|
console.warn("Failing security check: Error: " + err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadFile(url, cb, lastModified) {
|
||||||
|
// lastModified is a timestamp (ms since epoch); if provided, then the file
|
||||||
|
// will not be downloaded unless it is newer than this.
|
||||||
|
var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
||||||
|
.createInstance( Ci.nsIXMLHttpRequest );
|
||||||
|
req.open('GET', url, true);
|
||||||
|
if (lastModified != undefined) {
|
||||||
|
let d = new Date();
|
||||||
|
d.setTime(lastModified);
|
||||||
|
// example header: If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT
|
||||||
|
req.setRequestHeader("If-Modified-Since", d.toGMTString());
|
||||||
|
console.info("Setting if-modified-since header to " + d.toGMTString());
|
||||||
|
}
|
||||||
|
//Use binary mode to download jars TODO find a better switch
|
||||||
|
if (url.indexOf(".jar") == url.length - 4) {
|
||||||
|
console.info("Using binary mode to download jar file.");
|
||||||
|
req.overrideMimeType('text/plain; charset=x-user-defined');
|
||||||
|
}
|
||||||
|
req.onreadystatechange = function(aEvt) {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
if (req.status == 200) {
|
||||||
|
// check security channel:
|
||||||
|
if (verifyChannelSecurity(req.channel)) {
|
||||||
|
cb(req.responseText);
|
||||||
|
} else {
|
||||||
|
cb(null);
|
||||||
|
}
|
||||||
|
} else if (req.status == 304) {
|
||||||
|
// 304 is "Not Modified", which we can get because we send an
|
||||||
|
// If-Modified-Since header.
|
||||||
|
console.info("File " + url + " not modified; using cached version.");
|
||||||
|
cb(null);
|
||||||
|
// calling back with null lets the RemoteExperimentLoader know it should
|
||||||
|
// keep using the old cached version of the code.
|
||||||
|
} else {
|
||||||
|
// Some kind of error.
|
||||||
|
console.warn("Got a " + req.status + " error code downloading " + url);
|
||||||
|
cb(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
// example contents of extensions.testpilot.experiment.codeFs:
|
||||||
|
// {'fs': {"bookmark01/experiment": "<plain-text code @ bookmarks.js>"}}
|
||||||
|
// sample code
|
||||||
|
// example data:
|
||||||
|
// {'experiments': [{'name': 'Bookmark Experiment',
|
||||||
|
// 'filename': 'bookmarks.js'}]}
|
||||||
|
|
||||||
|
exports.RemoteExperimentLoader = function(logRepo, fileGetterFunction ) {
|
||||||
|
/* fileGetterFunction is an optional stub function for unit testing. Pass in
|
||||||
|
* nothing to have it use the default behavior of downloading the files from the
|
||||||
|
* Test Pilot server. FileGetterFunction must take (url, callback).*/
|
||||||
|
this._init(logRepo, fileGetterFunction);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.RemoteExperimentLoader.prototype = {
|
||||||
|
_init: function(logRepo, fileGetterFunction) {
|
||||||
|
this._logger = logRepo.getLogger("TestPilot.Loader");
|
||||||
|
this._expLogger = logRepo.getLogger("TestPilot.RemoteCode");
|
||||||
|
this._studyResults = [];
|
||||||
|
this._legacyStudies = [];
|
||||||
|
let prefs = require("preferences-service");
|
||||||
|
this._baseUrl = prefs.get(BASE_URL_PREF, "");
|
||||||
|
if (fileGetterFunction != undefined) {
|
||||||
|
this._fileGetter = fileGetterFunction;
|
||||||
|
} else {
|
||||||
|
this._fileGetter = downloadFile;
|
||||||
|
}
|
||||||
|
this._logger.trace("About to instantiate preferences store.");
|
||||||
|
this._jarStore = new JarStore();
|
||||||
|
this._experimentFileNames = [];
|
||||||
|
let self = this;
|
||||||
|
this._logger.trace("About to instantiate cuddlefish loader.");
|
||||||
|
this._refreshLoader();
|
||||||
|
// set up the unloading
|
||||||
|
require("unload").when( function() {
|
||||||
|
self._loader.unload();
|
||||||
|
});
|
||||||
|
this._logger.trace("Done instantiating remoteExperimentLoader.");
|
||||||
|
},
|
||||||
|
|
||||||
|
_refreshLoader: function() {
|
||||||
|
if (this._loader) {
|
||||||
|
this._loader.unload();
|
||||||
|
}
|
||||||
|
/* Pass in "TestPilot.experiment" logger as the console object for
|
||||||
|
* all remote modules loaded through cuddlefish, so they will log their
|
||||||
|
* stuff to the same file as all other modules. This logger is not
|
||||||
|
* technically a console object but as long as it has .debug, .info,
|
||||||
|
* .warn, and .error methods, it will work fine.*/
|
||||||
|
|
||||||
|
/* Use a composite file system here, compositing codeStorage and a new
|
||||||
|
* local file system so that securable modules loaded remotely can
|
||||||
|
* themselves require modules in the cuddlefish lib. */
|
||||||
|
let self = this;
|
||||||
|
this._loader = Cuddlefish.Loader(
|
||||||
|
{fs: new SecurableModule.CompositeFileSystem(
|
||||||
|
[self._jarStore, Cuddlefish.parentLoader.fs]),
|
||||||
|
console: this._expLogger
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
checkForUpdates: function(callback) {
|
||||||
|
/* Callback will be called with true or false
|
||||||
|
* to let us know whether there are any updates, so that client code can
|
||||||
|
* restart any experiment whose code has changed. */
|
||||||
|
let prefs = require("preferences-service");
|
||||||
|
let indexFileName = prefs.get("extensions.testpilot.indexFileName",
|
||||||
|
"index.json");
|
||||||
|
let self = this;
|
||||||
|
// Unload everything before checking for updates, to be sure we
|
||||||
|
// get the newest stuff.
|
||||||
|
this._logger.info("Unloading everything to prepare to check for updates.");
|
||||||
|
this._refreshLoader();
|
||||||
|
|
||||||
|
// Check for surveys and studies
|
||||||
|
let url = resolveUrl(self._baseUrl, indexFileName);
|
||||||
|
self._fileGetter(url, function onDone(data) {
|
||||||
|
if (data) {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
self._logger.warn("Error parsing index.json: " + e );
|
||||||
|
callback(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache study results...
|
||||||
|
self._studyResults = data.results;
|
||||||
|
self._legacyStudies = data.legacy;
|
||||||
|
|
||||||
|
/* Go through each file indicated in index.json, attempt to load it into
|
||||||
|
* codeStorage (replacing any older version there).
|
||||||
|
*/
|
||||||
|
let jarFiles = data.experiment_jars;
|
||||||
|
let numFilesToDload = jarFiles.length;
|
||||||
|
|
||||||
|
for each (let j in jarFiles) {
|
||||||
|
let filename = j.jarfile;
|
||||||
|
let hash = j.hash;
|
||||||
|
if (j.studyfile) {
|
||||||
|
self._experimentFileNames.push(j.studyfile);
|
||||||
|
}
|
||||||
|
self._logger.trace("I'm gonna go try to get the code for " + filename);
|
||||||
|
let modDate = self._jarStore.getFileModifiedDate(filename);
|
||||||
|
|
||||||
|
self._fileGetter(resolveUrl(self._baseUrl, filename),
|
||||||
|
function onDone(code) {
|
||||||
|
// code will be non-null if there is actually new code to download.
|
||||||
|
if (code) {
|
||||||
|
self._logger.info("Downloaded jar file " + filename);
|
||||||
|
self._jarStore.saveJarFile(filename, code, hash);
|
||||||
|
self._logger.trace("Saved code for: " + filename);
|
||||||
|
} else {
|
||||||
|
self._logger.info("Nothing to download for " + filename);
|
||||||
|
}
|
||||||
|
numFilesToDload--;
|
||||||
|
if (numFilesToDload == 0) {
|
||||||
|
self._logger.trace("Calling callback.");
|
||||||
|
callback(true);
|
||||||
|
}
|
||||||
|
}, modDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
self._logger.warn("Could not download index.json from test pilot server.");
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getExperiments: function() {
|
||||||
|
/* Load up and return all studies/surveys (not libraries)
|
||||||
|
* already stored in codeStorage. Returns a dict with key =
|
||||||
|
* the module name and value = the module object. */
|
||||||
|
this._logger.trace("GetExperiments called.");
|
||||||
|
let remoteExperiments = {};
|
||||||
|
for each (filename in this._experimentFileNames) {
|
||||||
|
this._logger.debug("GetExperiments is loading " + filename);
|
||||||
|
try {
|
||||||
|
remoteExperiments[filename] = this._loader.require(filename);
|
||||||
|
this._logger.info("Loaded " + filename + " OK.");
|
||||||
|
} catch(e) {
|
||||||
|
this._logger.warn("Error loading " + filename);
|
||||||
|
this._logger.warn(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return remoteExperiments;
|
||||||
|
},
|
||||||
|
|
||||||
|
getStudyResults: function() {
|
||||||
|
return this._studyResults;
|
||||||
|
},
|
||||||
|
|
||||||
|
getLegacyStudies: function() {
|
||||||
|
return this._legacyStudies;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO purge the pref store of anybody who has one.
|
||||||
|
|
||||||
|
// TODO i realized that right now there is no way for experiments
|
||||||
|
// on disk to get loaded if the index file is not accessible for
|
||||||
|
// any reason. getExperiments needs to be able to return names of
|
||||||
|
// experiment modules on disk even if connection to server fails. But
|
||||||
|
// we can't just load everything; some modules in the jar are not
|
||||||
|
// experiments. Right now the information as to which modules are
|
||||||
|
// experiments lives ONLY in index.json. What if we put it into the .jar
|
||||||
|
// file itself somehow? Like calling one of the files "study.js". Or
|
||||||
|
// "survey.js" Hey, that would be neat - one .jar file containing both
|
||||||
|
// the study.js and the survey.js. Or there could be a mini-manifest in the
|
||||||
|
// jar telling which files are experiments.
|
||||||
|
|
||||||
|
// TODO Also, if user has a study id foo that is not expired yet, and
|
||||||
|
// a LegacyStudy appears with the same id, they should keep their "real"
|
||||||
|
// version of id foo and not load the LegacyStudy version.
|
||||||
|
|
||||||
|
// TODO but once the study is expired, should delete the jar for it and
|
||||||
|
// just load the LegacyStudy version.
|
||||||
|
|
|
@ -0,0 +1,858 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Test Pilot.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Atul Varma <atul@mozilla.com>
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
* Raymond Lee <raymond@appcoast.com>
|
||||||
|
* Jorge Villalobos <jorge@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
EXPORTED_SYMBOLS = ["TestPilotSetup", "POPUP_SHOW_ON_NEW",
|
||||||
|
"POPUP_SHOW_ON_FINISH", "POPUP_SHOW_ON_RESULTS",
|
||||||
|
"ALWAYS_SUBMIT_DATA", "RUN_AT_ALL_PREF"];
|
||||||
|
|
||||||
|
const Cc = Components.classes;
|
||||||
|
const Ci = Components.interfaces;
|
||||||
|
const Cu = Components.utils;
|
||||||
|
|
||||||
|
const EXTENSION_ID = "testpilot@labs.mozilla.com";
|
||||||
|
const VERSION_PREF ="extensions.testpilot.lastversion";
|
||||||
|
const FIRST_RUN_PREF ="extensions.testpilot.firstRunUrl";
|
||||||
|
const RUN_AT_ALL_PREF = "extensions.testpilot.runStudies";
|
||||||
|
const POPUP_SHOW_ON_NEW = "extensions.testpilot.popup.showOnNewStudy";
|
||||||
|
const POPUP_SHOW_ON_FINISH = "extensions.testpilot.popup.showOnStudyFinished";
|
||||||
|
const POPUP_SHOW_ON_RESULTS = "extensions.testpilot.popup.showOnNewResults";
|
||||||
|
const POPUP_CHECK_INTERVAL = "extensions.testpilot.popup.delayAfterStartup";
|
||||||
|
const POPUP_REMINDER_INTERVAL = "extensions.testpilot.popup.timeBetweenChecks";
|
||||||
|
const ALWAYS_SUBMIT_DATA = "extensions.testpilot.alwaysSubmitData";
|
||||||
|
const LOG_FILE_NAME = "TestPilotErrorLog.log";
|
||||||
|
|
||||||
|
let TestPilotSetup = {
|
||||||
|
didReminderAfterStartup: false,
|
||||||
|
startupComplete: false,
|
||||||
|
_shortTimer: null,
|
||||||
|
_longTimer: null,
|
||||||
|
_remoteExperimentLoader: null,
|
||||||
|
taskList: [],
|
||||||
|
version: "",
|
||||||
|
|
||||||
|
// Lazy initializers:
|
||||||
|
__application: null,
|
||||||
|
get _application() {
|
||||||
|
if (this.__application == null) {
|
||||||
|
this.__application = Cc["@mozilla.org/fuel/application;1"]
|
||||||
|
.getService(Ci.fuelIApplication);
|
||||||
|
}
|
||||||
|
return this.__application;
|
||||||
|
},
|
||||||
|
|
||||||
|
get _prefs() {
|
||||||
|
return this._application.prefs;
|
||||||
|
},
|
||||||
|
|
||||||
|
__loader: null,
|
||||||
|
get _loader() {
|
||||||
|
if (this.__loader == null) {
|
||||||
|
let Cuddlefish = {};
|
||||||
|
Components.utils.import("resource://testpilot/modules/lib/cuddlefish.js",
|
||||||
|
Cuddlefish);
|
||||||
|
let repo = this._logRepo;
|
||||||
|
this.__loader = new Cuddlefish.Loader(
|
||||||
|
{rootPaths: ["resource://testpilot/modules/",
|
||||||
|
"resource://testpilot/modules/lib/"],
|
||||||
|
console: repo.getLogger("TestPilot.Loader")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.__loader;
|
||||||
|
},
|
||||||
|
|
||||||
|
__feedbackManager: null,
|
||||||
|
get _feedbackManager() {
|
||||||
|
if (this.__feedbackManager == null) {
|
||||||
|
let FeedbackModule = {};
|
||||||
|
Cu.import("resource://testpilot/modules/feedback.js", FeedbackModule);
|
||||||
|
this.__feedbackManager = FeedbackModule.FeedbackManager;
|
||||||
|
}
|
||||||
|
return this.__feedbackManager;
|
||||||
|
},
|
||||||
|
|
||||||
|
__dataStoreModule: null,
|
||||||
|
get _dataStoreModule() {
|
||||||
|
if (this.__dataStoreModule == null) {
|
||||||
|
this.__dataStoreModule = {};
|
||||||
|
Cu.import("resource://testpilot/modules/experiment_data_store.js",
|
||||||
|
this._dataStoreModule);
|
||||||
|
}
|
||||||
|
return this.__dataStoreModule;
|
||||||
|
},
|
||||||
|
|
||||||
|
__extensionUpdater: null,
|
||||||
|
get _extensionUpdater() {
|
||||||
|
if (this.__extensionUpdater == null) {
|
||||||
|
let ExUpdate = {};
|
||||||
|
Cu.import("resource://testpilot/modules/extension-update.js",
|
||||||
|
ExUpdate);
|
||||||
|
this.__extensionUpdater = ExUpdate.TestPilotExtensionUpdate;
|
||||||
|
}
|
||||||
|
return this.__extensionUpdater;
|
||||||
|
},
|
||||||
|
|
||||||
|
__logRepo: null,
|
||||||
|
get _logRepo() {
|
||||||
|
// Note: This hits the disk so it's an expensive operation; don't call it
|
||||||
|
// on startup.
|
||||||
|
if (this.__logRepo == null) {
|
||||||
|
let Log4MozModule = {};
|
||||||
|
Cu.import("resource://testpilot/modules/log4moz.js", Log4MozModule);
|
||||||
|
let props = Cc["@mozilla.org/file/directory_service;1"].
|
||||||
|
getService(Ci.nsIProperties);
|
||||||
|
let logFile = props.get("ProfD", Components.interfaces.nsIFile);
|
||||||
|
logFile.append(LOG_FILE_NAME);
|
||||||
|
let formatter = new Log4MozModule.Log4Moz.BasicFormatter;
|
||||||
|
let root = Log4MozModule.Log4Moz.repository.rootLogger;
|
||||||
|
root.level = Log4MozModule.Log4Moz.Level["All"];
|
||||||
|
let appender = new Log4MozModule.Log4Moz.RotatingFileAppender(logFile, formatter);
|
||||||
|
root.addAppender(appender);
|
||||||
|
this.__logRepo = Log4MozModule.Log4Moz.repository;
|
||||||
|
}
|
||||||
|
return this.__logRepo;
|
||||||
|
},
|
||||||
|
|
||||||
|
__logger: null,
|
||||||
|
get _logger() {
|
||||||
|
if (this.__logger == null) {
|
||||||
|
this.__logger = this._logRepo.getLogger("TestPilot.Setup");
|
||||||
|
}
|
||||||
|
return this.__logger;
|
||||||
|
},
|
||||||
|
|
||||||
|
__taskModule: null,
|
||||||
|
get _taskModule() {
|
||||||
|
if (this.__taskModule == null) {
|
||||||
|
this.__taskModule = {};
|
||||||
|
Cu.import("resource://testpilot/modules/tasks.js", this.__taskModule);
|
||||||
|
}
|
||||||
|
return this.__taskModule;
|
||||||
|
},
|
||||||
|
|
||||||
|
__stringBundle: null,
|
||||||
|
get _stringBundle() {
|
||||||
|
if (this.__stringBundle == null) {
|
||||||
|
this.__stringBundle =
|
||||||
|
Cc["@mozilla.org/intl/stringbundle;1"].
|
||||||
|
getService(Ci.nsIStringBundleService).
|
||||||
|
createBundle("chrome://testpilot/locale/main.properties");
|
||||||
|
}
|
||||||
|
return this.__stringBundle;
|
||||||
|
},
|
||||||
|
|
||||||
|
__obs: null,
|
||||||
|
get _obs() {
|
||||||
|
if (this.__obs == null) {
|
||||||
|
this.__obs = this._loader.require("observer-service");
|
||||||
|
}
|
||||||
|
return this.__obs;
|
||||||
|
},
|
||||||
|
|
||||||
|
_isFfx4BetaVersion: function TPS__isFfx4BetaVersion() {
|
||||||
|
let result = Cc["@mozilla.org/xpcom/version-comparator;1"]
|
||||||
|
.getService(Ci.nsIVersionComparator)
|
||||||
|
.compare("3.7a1pre", this._application.version);
|
||||||
|
if (result < 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_setPrefDefaultsForVersion: function TPS__setPrefDefaultsForVersion() {
|
||||||
|
/* A couple of preferences need different default values depending on
|
||||||
|
* whether we're in the Firefox 4 beta version or the standalone TP version
|
||||||
|
*/
|
||||||
|
let ps = Cc["@mozilla.org/preferences-service;1"]
|
||||||
|
.getService(Ci.nsIPrefService);
|
||||||
|
let prefBranch = ps.getDefaultBranch("");
|
||||||
|
/* note we're setting default values, not current values -- these
|
||||||
|
* get overridden by any user set values. */
|
||||||
|
if (this._isFfx4BetaVersion()) {
|
||||||
|
prefBranch.setBoolPref(POPUP_SHOW_ON_NEW, true);
|
||||||
|
prefBranch.setIntPref(POPUP_CHECK_INTERVAL, 600000);
|
||||||
|
} else {
|
||||||
|
prefBranch.setBoolPref(POPUP_SHOW_ON_NEW, false);
|
||||||
|
prefBranch.setIntPref(POPUP_CHECK_INTERVAL, 180000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_setUpToolbarFeedbackButton: function TPS_toolbarFeedbackButton() {
|
||||||
|
/* If this is first run, and it's ffx4 beta version, and the feedback
|
||||||
|
* button is not in the expected place, put it there!
|
||||||
|
* (copied from MozReporterButtons extension) */
|
||||||
|
let logger = this._logger;
|
||||||
|
try {
|
||||||
|
let win = this._getFrontBrowserWindow();
|
||||||
|
let firefoxnav = win.document.getElementById("nav-bar");
|
||||||
|
let curSet = firefoxnav.currentSet;
|
||||||
|
|
||||||
|
if (-1 == curSet.indexOf("feedback-menu-button")) {
|
||||||
|
logger.info("Feedback toolbar button not present: Adding it.");
|
||||||
|
// place the buttons after the search box.
|
||||||
|
let newSet = curSet + ",feedback-menu-button";
|
||||||
|
|
||||||
|
firefoxnav.setAttribute("currentset", newSet);
|
||||||
|
firefoxnav.currentSet = newSet;
|
||||||
|
win.document.persist("nav-bar", "currentset");
|
||||||
|
// if you don't do the following call, funny things happen.
|
||||||
|
try {
|
||||||
|
BrowserToolboxCustomizeDone(true);
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("Error in setUpToolbarFeedbackButton: " + e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
globalStartup: function TPS__doGlobalSetup() {
|
||||||
|
// Only ever run this stuff ONCE, on the first window restore.
|
||||||
|
// Should get called by the Test Pilot component.
|
||||||
|
let logger = this._logger;
|
||||||
|
logger.trace("TestPilotSetup.globalStartup was called.");
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._setPrefDefaultsForVersion();
|
||||||
|
if (!this._prefs.getValue(RUN_AT_ALL_PREF, true)) {
|
||||||
|
logger.trace("Test Pilot globally disabled: Not starting up.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up observation for task state changes
|
||||||
|
var self = this;
|
||||||
|
this._obs.add("testpilot:task:changed", this.onTaskStatusChanged, self);
|
||||||
|
this._obs.add(
|
||||||
|
"testpilot:task:dataAutoSubmitted", this._onTaskDataAutoSubmitted, self);
|
||||||
|
// Set up observation for application shutdown.
|
||||||
|
this._obs.add("quit-application", this.globalShutdown, self);
|
||||||
|
// Set up observation for enter/exit private browsing:
|
||||||
|
this._obs.add("private-browsing", this.onPrivateBrowsingMode, self);
|
||||||
|
|
||||||
|
// Set up timers to remind user x minutes after startup
|
||||||
|
// and once per day thereafter. Use nsITimer so it doesn't belong to
|
||||||
|
// any one window.
|
||||||
|
logger.trace("Setting interval for showing reminders...");
|
||||||
|
|
||||||
|
this._shortTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||||
|
this._shortTimer.initWithCallback(
|
||||||
|
{ notify: function(timer) { self._doHousekeeping();} },
|
||||||
|
this._prefs.getValue(POPUP_CHECK_INTERVAL, 180000),
|
||||||
|
Ci.nsITimer.TYPE_REPEATING_SLACK
|
||||||
|
);
|
||||||
|
this._longTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||||
|
this._longTimer.initWithCallback(
|
||||||
|
{ notify: function(timer) {
|
||||||
|
self.reloadRemoteExperiments(function() {
|
||||||
|
self._notifyUserOfTasks();
|
||||||
|
});
|
||||||
|
}}, this._prefs.getValue(POPUP_REMINDER_INTERVAL, 86400000),
|
||||||
|
Ci.nsITimer.TYPE_REPEATING_SLACK);
|
||||||
|
|
||||||
|
this.getVersion(function() {
|
||||||
|
// Show first run page (in front window) if newly installed or upgraded.
|
||||||
|
let currVersion = self._prefs.getValue(VERSION_PREF, "firstrun");
|
||||||
|
|
||||||
|
if (currVersion != self.version) {
|
||||||
|
if(!self._isFfx4BetaVersion()) {
|
||||||
|
self._prefs.setValue(VERSION_PREF, self.version);
|
||||||
|
let browser = self._getFrontBrowserWindow().getBrowser();
|
||||||
|
let url = self._prefs.getValue(FIRST_RUN_PREF, "");
|
||||||
|
let tab = browser.addTab(url);
|
||||||
|
browser.selectedTab = tab;
|
||||||
|
} else {
|
||||||
|
// Don't show first run page in ffx4 beta version... but do
|
||||||
|
// set up the Feedback button in the toolbar.
|
||||||
|
self._setUpToolbarFeedbackButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install tasks. (This requires knowing the version, so it is
|
||||||
|
// inside the callback from getVersion.)
|
||||||
|
self.checkForTasks(function() {
|
||||||
|
/* Callback to complete startup after we finish
|
||||||
|
* checking for tasks. */
|
||||||
|
self.startupComplete = true;
|
||||||
|
logger.trace("I'm in the callback from checkForTasks.");
|
||||||
|
// Send startup message to each task:
|
||||||
|
for (let i = 0; i < self.taskList.length; i++) {
|
||||||
|
self.taskList[i].onAppStartup();
|
||||||
|
}
|
||||||
|
self._obs.notify("testpilot:startup:complete", "", null);
|
||||||
|
/* onWindowLoad gets called once for each window,
|
||||||
|
* but only after we fire this notification. */
|
||||||
|
logger.trace("Testpilot startup complete.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
logger.error("Error in testPilot startup: " + e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
globalShutdown: function TPS_globalShutdown() {
|
||||||
|
let logger = this._logger;
|
||||||
|
logger.trace("Global shutdown. Unregistering everything.");
|
||||||
|
let self = this;
|
||||||
|
for (let i = 0; i < self.taskList.length; i++) {
|
||||||
|
self.taskList[i].onAppShutdown();
|
||||||
|
self.taskList[i].onExperimentShutdown();
|
||||||
|
}
|
||||||
|
this.taskList = [];
|
||||||
|
this._loader.unload();
|
||||||
|
this._obs.remove("testpilot:task:changed", this.onTaskStatusChanged, self);
|
||||||
|
this._obs.remove(
|
||||||
|
"testpilot:task:dataAutoSubmitted", this._onTaskDataAutoSubmitted, self);
|
||||||
|
this._obs.remove("quit-application", this.globalShutdown, self);
|
||||||
|
this._obs.remove("private-browsing", this.onPrivateBrowsingMode, self);
|
||||||
|
this._loader.unload();
|
||||||
|
this._shortTimer.cancel();
|
||||||
|
this._longTimer.cancel();
|
||||||
|
logger.trace("Done unregistering everything.");
|
||||||
|
},
|
||||||
|
|
||||||
|
_getFrontBrowserWindow: function TPS__getFrontWindow() {
|
||||||
|
let wm = Cc["@mozilla.org/appshell/window-mediator;1"].
|
||||||
|
getService(Ci.nsIWindowMediator);
|
||||||
|
// TODO Is "most recent" the same as "front"?
|
||||||
|
return wm.getMostRecentWindow("navigator:browser");
|
||||||
|
},
|
||||||
|
|
||||||
|
onPrivateBrowsingMode: function TPS_onPrivateBrowsingMode(topic, data) {
|
||||||
|
for (let i = 0; i < this.taskList.length; i++) {
|
||||||
|
if (data == "enter") {
|
||||||
|
this.taskList[i].onEnterPrivateBrowsing();
|
||||||
|
} else if (data == "exit") {
|
||||||
|
this.taskList[i].onExitPrivateBrowsing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onWindowUnload: function TPS__onWindowRegistered(window) {
|
||||||
|
this._logger.trace("Called TestPilotSetup.onWindow unload!");
|
||||||
|
for (let i = 0; i < this.taskList.length; i++) {
|
||||||
|
this.taskList[i].onWindowClosed(window);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onWindowLoad: function TPS_onWindowLoad(window) {
|
||||||
|
this._logger.trace("Called TestPilotSetup.onWindowLoad!");
|
||||||
|
// Run this stuff once per window...
|
||||||
|
let self = this;
|
||||||
|
|
||||||
|
// Register listener for URL loads, that will notify all tasks about
|
||||||
|
// new page:
|
||||||
|
let appcontent = window.document.getElementById("appcontent");
|
||||||
|
if (appcontent) {
|
||||||
|
appcontent.addEventListener("DOMContentLoaded", function(event) {
|
||||||
|
let newUrl = event.originalTarget.URL;
|
||||||
|
self._feedbackManager.fillInFeedbackPage(newUrl, window);
|
||||||
|
for (i = 0; i < self.taskList.length; i++) {
|
||||||
|
self.taskList[i].onUrlLoad(newUrl, event);
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let each task know about the new window.
|
||||||
|
for (let i = 0; i < this.taskList.length; i++) {
|
||||||
|
this.taskList[i].onNewWindow(window);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addTask: function TPS_addTask(testPilotTask) {
|
||||||
|
// TODO raise some kind of exception if a task with the same ID already
|
||||||
|
// exists. No excuse to ever be running two copies of the same task.
|
||||||
|
this.taskList.push(testPilotTask);
|
||||||
|
},
|
||||||
|
|
||||||
|
_showNotification: function TPS__showNotification(task, fragile, text, title,
|
||||||
|
iconClass, showSubmit,
|
||||||
|
showAlwaysSubmitCheckbox,
|
||||||
|
linkText, linkUrl,
|
||||||
|
isExtensionUpdate) {
|
||||||
|
// If there are multiple windows, show notifications in the frontmost
|
||||||
|
// window.
|
||||||
|
let doc = this._getFrontBrowserWindow().document;
|
||||||
|
let popup = doc.getElementById("pilot-notification-popup");
|
||||||
|
|
||||||
|
let anchor;
|
||||||
|
if (this._isFfx4BetaVersion()) {
|
||||||
|
/* If we're in the Ffx4Beta version, popups come down from feedback
|
||||||
|
* button, but if we're in the standalone extension version, they
|
||||||
|
* come up from status bar icon. */
|
||||||
|
anchor = doc.getElementById("feedback-menu-button");
|
||||||
|
popup.setAttribute("class", "tail-up");
|
||||||
|
} else {
|
||||||
|
anchor = doc.getElementById("pilot-notifications-button");
|
||||||
|
popup.setAttribute("class", "tail-down");
|
||||||
|
}
|
||||||
|
let textLabel = doc.getElementById("pilot-notification-text");
|
||||||
|
let titleLabel = doc.getElementById("pilot-notification-title");
|
||||||
|
let icon = doc.getElementById("pilot-notification-icon");
|
||||||
|
let submitBtn = doc.getElementById("pilot-notification-submit");
|
||||||
|
let closeBtn = doc.getElementById("pilot-notification-close");
|
||||||
|
let link = doc.getElementById("pilot-notification-link");
|
||||||
|
let alwaysSubmitCheckbox =
|
||||||
|
doc.getElementById("pilot-notification-always-submit-checkbox");
|
||||||
|
let self = this;
|
||||||
|
|
||||||
|
// Set all appropriate attributes on popup:
|
||||||
|
if (isExtensionUpdate) {
|
||||||
|
popup.setAttribute("tpisextensionupdate", "true");
|
||||||
|
}
|
||||||
|
popup.setAttribute("noautohide", !fragile);
|
||||||
|
titleLabel.setAttribute("value", title);
|
||||||
|
while (textLabel.lastChild) {
|
||||||
|
textLabel.removeChild(textLabel.lastChild);
|
||||||
|
}
|
||||||
|
textLabel.appendChild(doc.createTextNode(text));
|
||||||
|
if (iconClass) {
|
||||||
|
// css will set the image url based on the class.
|
||||||
|
icon.setAttribute("class", iconClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
alwaysSubmitCheckbox.setAttribute("hidden", !showAlwaysSubmitCheckbox);
|
||||||
|
if (showSubmit) {
|
||||||
|
if (isExtensionUpdate) {
|
||||||
|
submitBtn.setAttribute("label",
|
||||||
|
this._stringBundle.GetStringFromName(
|
||||||
|
"testpilot.notification.update"));
|
||||||
|
submitBtn.onclick = function() {
|
||||||
|
this._extensionUpdater.check(EXTENSION_ID);
|
||||||
|
self._hideNotification();
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
submitBtn.setAttribute("label",
|
||||||
|
this._stringBundle.GetStringFromName("testpilot.submit"));
|
||||||
|
// Functionality for submit button:
|
||||||
|
submitBtn.onclick = function() {
|
||||||
|
self._hideNotification();
|
||||||
|
if (showAlwaysSubmitCheckbox && alwaysSubmitCheckbox.checked) {
|
||||||
|
self._prefs.setValue(ALWAYS_SUBMIT_DATA, true);
|
||||||
|
}
|
||||||
|
task.upload( function(success) {
|
||||||
|
if (success) {
|
||||||
|
self._showNotification(
|
||||||
|
task, true,
|
||||||
|
self._stringBundle.GetStringFromName(
|
||||||
|
"testpilot.notification.thankYouForUploadingData.message"),
|
||||||
|
self._stringBundle.GetStringFromName(
|
||||||
|
"testpilot.notification.thankYouForUploadingData"),
|
||||||
|
"study-submitted", false, false,
|
||||||
|
self._stringBundle.GetStringFromName("testpilot.moreInfo"),
|
||||||
|
task.defaultUrl);
|
||||||
|
} else {
|
||||||
|
// TODO any point in showing an error message here?
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
submitBtn.setAttribute("hidden", !showSubmit);
|
||||||
|
|
||||||
|
// Create the link if specified:
|
||||||
|
if (linkText && (linkUrl || task)) {
|
||||||
|
link.setAttribute("value", linkText);
|
||||||
|
link.setAttribute("class", "notification-link");
|
||||||
|
link.onclick = function(event) {
|
||||||
|
if (event.button == 0) {
|
||||||
|
if (task) {
|
||||||
|
task.loadPage();
|
||||||
|
} else {
|
||||||
|
self._openChromeless(linkUrl);
|
||||||
|
}
|
||||||
|
self._hideNotification();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
link.setAttribute("hidden", false);
|
||||||
|
} else {
|
||||||
|
link.setAttribute("hidden", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeBtn.onclick = function() {
|
||||||
|
self._hideNotification();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show the popup:
|
||||||
|
popup.hidden = false;
|
||||||
|
popup.setAttribute("open", "true");
|
||||||
|
popup.openPopup( anchor, "after_end");
|
||||||
|
},
|
||||||
|
|
||||||
|
_openChromeless: function TPS__openChromeless(url) {
|
||||||
|
let window = this._getFrontBrowserWindow();
|
||||||
|
window.TestPilotWindowUtils.openChromeless(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
_hideNotification: function TPS__hideNotification() {
|
||||||
|
let window = this._getFrontBrowserWindow();
|
||||||
|
let popup = window.document.getElementById("pilot-notification-popup");
|
||||||
|
popup.hidden = true;
|
||||||
|
popup.setAttribute("open", "false");
|
||||||
|
popup.removeAttribute("tpisextensionupdate");
|
||||||
|
popup.hidePopup();
|
||||||
|
},
|
||||||
|
|
||||||
|
_isShowingUpdateNotification : function() {
|
||||||
|
let window = this._getFrontBrowserWindow();
|
||||||
|
let popup = window.document.getElementById("pilot-notification-popup");
|
||||||
|
|
||||||
|
return popup.hasAttribute("tpisextensionupdate");
|
||||||
|
},
|
||||||
|
|
||||||
|
_notifyUserOfTasks: function TPS__notifyUser() {
|
||||||
|
// Check whether there are tasks needing attention, and if any are
|
||||||
|
// found, show the popup door-hanger thingy.
|
||||||
|
let i, task;
|
||||||
|
let TaskConstants = this._taskModule.TaskConstants;
|
||||||
|
|
||||||
|
// if showing extension update notification, don't do anything.
|
||||||
|
if (this._isShowingUpdateNotification()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highest priority is if there is a finished test (needs a decision)
|
||||||
|
if (this._prefs.getValue(POPUP_SHOW_ON_FINISH, false)) {
|
||||||
|
for (i = 0; i < this.taskList.length; i++) {
|
||||||
|
task = this.taskList[i];
|
||||||
|
if (task.status == TaskConstants.STATUS_FINISHED) {
|
||||||
|
if (!this._prefs.getValue(ALWAYS_SUBMIT_DATA, false)) {
|
||||||
|
this._showNotification(
|
||||||
|
task, false,
|
||||||
|
this._stringBundle.formatStringFromName(
|
||||||
|
"testpilot.notification.readyToSubmit.message", [task.title],
|
||||||
|
1),
|
||||||
|
this._stringBundle.GetStringFromName(
|
||||||
|
"testpilot.notification.readyToSubmit"),
|
||||||
|
"study-finished", true, true,
|
||||||
|
this._stringBundle.GetStringFromName("testpilot.moreInfo"),
|
||||||
|
task.defaultUrl);
|
||||||
|
// We return after showing something, because it only makes
|
||||||
|
// sense to show one notification at a time!
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's no finished test, next highest priority is new tests that
|
||||||
|
// are starting...
|
||||||
|
if (this._prefs.getValue(POPUP_SHOW_ON_NEW, false)) {
|
||||||
|
for (i = 0; i < this.taskList.length; i++) {
|
||||||
|
task = this.taskList[i];
|
||||||
|
if (task.status == TaskConstants.STATUS_STARTING ||
|
||||||
|
task.status == TaskConstants.STATUS_NEW) {
|
||||||
|
if (task.taskType == TaskConstants.TYPE_EXPERIMENT) {
|
||||||
|
this._showNotification(
|
||||||
|
task, true,
|
||||||
|
this._stringBundle.formatStringFromName(
|
||||||
|
"testpilot.notification.newTestPilotStudy.message",
|
||||||
|
[task.title], 1),
|
||||||
|
this._stringBundle.GetStringFromName(
|
||||||
|
"testpilot.notification.newTestPilotStudy"),
|
||||||
|
"new-study", false, false,
|
||||||
|
this._stringBundle.GetStringFromName("testpilot.moreInfo"),
|
||||||
|
task.defaultUrl);
|
||||||
|
// Having shown the notification, update task status so that this
|
||||||
|
// notification won't be shown again.
|
||||||
|
task.changeStatus(TaskConstants.STATUS_IN_PROGRESS, true);
|
||||||
|
return;
|
||||||
|
} else if (task.taskType == TaskConstants.TYPE_SURVEY) {
|
||||||
|
this._showNotification(
|
||||||
|
task, true,
|
||||||
|
this._stringBundle.formatStringFromName(
|
||||||
|
"testpilot.notification.newTestPilotSurvey.message",
|
||||||
|
[task.title], 1),
|
||||||
|
this._stringBundle.GetStringFromName(
|
||||||
|
"testpilot.notification.newTestPilotSurvey"),
|
||||||
|
"new-study", false, false,
|
||||||
|
this._stringBundle.GetStringFromName("testpilot.moreInfo"),
|
||||||
|
task.defaultUrl);
|
||||||
|
task.changeStatus(TaskConstants.STATUS_IN_PROGRESS, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And finally, new experiment results:
|
||||||
|
if (this._prefs.getValue(POPUP_SHOW_ON_RESULTS, false)) {
|
||||||
|
for (i = 0; i < this.taskList.length; i++) {
|
||||||
|
task = this.taskList[i];
|
||||||
|
if (task.taskType == TaskConstants.TYPE_RESULTS &&
|
||||||
|
task.status == TaskConstants.STATUS_NEW) {
|
||||||
|
this._showNotification(
|
||||||
|
task, true,
|
||||||
|
this._stringBundle.formatStringFromName(
|
||||||
|
"testpilot.notification.newTestPilotResults.message",
|
||||||
|
[task.title], 1),
|
||||||
|
this._stringBundle.GetStringFromName(
|
||||||
|
"testpilot.notification.newTestPilotResults"),
|
||||||
|
"new-results", false, false,
|
||||||
|
this._stringBundle.GetStringFromName("testpilot.moreInfo"),
|
||||||
|
task.defaultUrl);
|
||||||
|
// Having shown the notification, advance the status of the
|
||||||
|
// results, so that this notification won't be shown again
|
||||||
|
task.changeStatus(TaskConstants.STATUS_ARCHIVED, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_doHousekeeping: function TPS__doHousekeeping() {
|
||||||
|
// check date on all tasks:
|
||||||
|
for (let i = 0; i < this.taskList.length; i++) {
|
||||||
|
let task = this.taskList[i];
|
||||||
|
task.checkDate();
|
||||||
|
}
|
||||||
|
// Do a full reminder -- but at most once per browser session
|
||||||
|
if (!this.didReminderAfterStartup) {
|
||||||
|
this._logger.trace("Doing reminder after startup...");
|
||||||
|
this.didReminderAfterStartup = true;
|
||||||
|
this._notifyUserOfTasks();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onTaskStatusChanged: function TPS_onTaskStatusChanged() {
|
||||||
|
this._notifyUserOfTasks();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onTaskDataAutoSubmitted: function(subject, data) {
|
||||||
|
this._showNotification(
|
||||||
|
subject, true,
|
||||||
|
this._stringBundle.formatStringFromName(
|
||||||
|
"testpilot.notification.autoUploadedData.message",
|
||||||
|
[subject.title], 1),
|
||||||
|
this._stringBundle.GetStringFromName(
|
||||||
|
"testpilot.notification.autoUploadedData"),
|
||||||
|
"study-submitted", false, false,
|
||||||
|
this._stringBundle.GetStringFromName("testpilot.moreInfo"),
|
||||||
|
subject.defaultUrl);
|
||||||
|
},
|
||||||
|
|
||||||
|
getVersion: function TPS_getVersion(callback) {
|
||||||
|
// Application.extensions undefined in Firefox 4; will use the new
|
||||||
|
// asynchrounous API, store string in this.version, and call the
|
||||||
|
// callback when done.
|
||||||
|
if (this._application.extensions) {
|
||||||
|
this.version = this._application.extensions.get(EXTENSION_ID).version;
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
let self = this;
|
||||||
|
self._application.getExtensions(function(extensions) {
|
||||||
|
self.version = extensions.get(EXTENSION_ID).version;
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_isNewerThanMe: function TPS__isNewerThanMe(versionString) {
|
||||||
|
let result = Cc["@mozilla.org/xpcom/version-comparator;1"]
|
||||||
|
.getService(Ci.nsIVersionComparator)
|
||||||
|
.compare(this.version, versionString);
|
||||||
|
if (result < 0) {
|
||||||
|
return true; // versionString is newer than my version
|
||||||
|
} else {
|
||||||
|
return false; // versionString is the same as or older than my version
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_isNewerThanFirefox: function TPS__isNewerThanFirefox(versionString) {
|
||||||
|
let result = Cc["@mozilla.org/xpcom/version-comparator;1"]
|
||||||
|
.getService(Ci.nsIVersionComparator)
|
||||||
|
.compare(self._application.version, versionString);
|
||||||
|
if (result < 0) {
|
||||||
|
return true; // versionString is newer than Firefox
|
||||||
|
} else {
|
||||||
|
return false; // versionString is the same as or older than Firefox
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_experimentRequirementsAreMet: function TPS__requirementsMet(experiment) {
|
||||||
|
// Returns true if we we meet the requirements to run this experiment
|
||||||
|
// (e.g. meet the minimum Test Pilot version and Firefox version)
|
||||||
|
// false if not.
|
||||||
|
// If the experiment doesn't specify minimum versions, attempt to run it.
|
||||||
|
let logger = this._logger;
|
||||||
|
try {
|
||||||
|
let minTpVer, minFxVer, expName;
|
||||||
|
if (experiment.experimentInfo) {
|
||||||
|
minTpVer = experiment.experimentInfo.minTPVersion;
|
||||||
|
minFxVer = experiment.experimentInfo.minFXVersion;
|
||||||
|
expName = experiment.experimentInfo.testName;
|
||||||
|
} else if (experiment.surveyInfo) {
|
||||||
|
minTpVer = experiment.surveyInfo.minTPVersion;
|
||||||
|
minFxVer = experiment.surveyInfo.minFXVersion;
|
||||||
|
expName = experiment.surveyInfo.surveyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimum test pilot version:
|
||||||
|
if (minTpVer && this._isNewerThanMe(minTpVer)) {
|
||||||
|
logger.warn("Not loading " + expName);
|
||||||
|
logger.warn("Because it requires Test Pilot version " + minTpVer);
|
||||||
|
|
||||||
|
// Let user know there is a newer version of Test Pilot available:
|
||||||
|
if (!this._isShowingUpdateNotification()) {
|
||||||
|
this._showNotification(
|
||||||
|
null, false,
|
||||||
|
this._stringBundle.GetStringFromName(
|
||||||
|
"testpilot.notification.extensionUpdate.message"),
|
||||||
|
this._stringBundle.GetStringFromName(
|
||||||
|
"testpilot.notification.extensionUpdate"),
|
||||||
|
"update-extension", true, false, "", "", true);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimum firefox version:
|
||||||
|
if (minFxVer && this._isNewerThanFirefox(minFxVer)) {
|
||||||
|
logger.warn("Not loading " + expName);
|
||||||
|
logger.warn("Because it requires Firefox version " + minFxVer);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("Error in requirements check " + expName + ": " + e);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
checkForTasks: function TPS_checkForTasks(callback) {
|
||||||
|
let logger = this._logger;
|
||||||
|
if (! this._remoteExperimentLoader ) {
|
||||||
|
logger.trace("Now requiring remote experiment loader:");
|
||||||
|
let remoteLoaderModule = this._loader.require("remote-experiment-loader");
|
||||||
|
logger.trace("Now instantiating remoteExperimentLoader:");
|
||||||
|
let rel = new remoteLoaderModule.RemoteExperimentLoader(this._logRepo);
|
||||||
|
this._remoteExperimentLoader = rel;
|
||||||
|
}
|
||||||
|
|
||||||
|
let self = this;
|
||||||
|
this._remoteExperimentLoader.checkForUpdates(
|
||||||
|
function(success) {
|
||||||
|
logger.info("Getting updated experiments... Success? " + success);
|
||||||
|
// Actually, we do exactly the same thing whether we succeeded in
|
||||||
|
// downloading new contents or not...
|
||||||
|
let experiments = self._remoteExperimentLoader.getExperiments();
|
||||||
|
|
||||||
|
for (let filename in experiments) {
|
||||||
|
if (!self._experimentRequirementsAreMet(experiments[filename])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// The try-catch ensures that if something goes wrong in loading one
|
||||||
|
// experiment, the other experiments after that one still get loaded.
|
||||||
|
logger.trace("Attempting to load experiment " + filename);
|
||||||
|
|
||||||
|
let task;
|
||||||
|
// Could be a survey: check if surveyInfo is exported:
|
||||||
|
if (experiments[filename].surveyInfo != undefined) {
|
||||||
|
let sInfo = experiments[filename].surveyInfo;
|
||||||
|
// If it supplies questions, it's a built-in survey.
|
||||||
|
// If not, it's a web-based survey.
|
||||||
|
if (!sInfo.surveyQuestions) {
|
||||||
|
task = new self._taskModule.TestPilotWebSurvey(sInfo);
|
||||||
|
} else {
|
||||||
|
task = new self._taskModule.TestPilotBuiltinSurvey(sInfo);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This one must be an experiment.
|
||||||
|
let expInfo = experiments[filename].experimentInfo;
|
||||||
|
let dsInfo = experiments[filename].dataStoreInfo;
|
||||||
|
let dataStore = new self._dataStoreModule.ExperimentDataStore(
|
||||||
|
dsInfo.fileName, dsInfo.tableName, dsInfo.columns );
|
||||||
|
let webContent = experiments[filename].webContent;
|
||||||
|
task = new self._taskModule.TestPilotExperiment(expInfo,
|
||||||
|
dataStore,
|
||||||
|
experiments[filename].handlers,
|
||||||
|
webContent);
|
||||||
|
}
|
||||||
|
self.addTask(task);
|
||||||
|
logger.info("Loaded task " + filename);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn("Failed to load task " + filename + ": " + e);
|
||||||
|
}
|
||||||
|
} // end for filename in experiments
|
||||||
|
|
||||||
|
// Handling new results is much simpler:
|
||||||
|
let results = self._remoteExperimentLoader.getStudyResults();
|
||||||
|
for (let r in results) {
|
||||||
|
let studyResult = new self._taskModule.TestPilotStudyResults(results[r]);
|
||||||
|
self.addTask(studyResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legacy studies = stuff we no longer have the code for, but
|
||||||
|
* if the user participated in it we want to keep that metadata. */
|
||||||
|
let legacyStudies = self._remoteExperimentLoader.getLegacyStudies();
|
||||||
|
for (let l in legacyStudies) {
|
||||||
|
let legacyStudy = new self._taskModule.TestPilotLegacyStudy(legacyStudies[l]);
|
||||||
|
self.addTask(legacyStudy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
reloadRemoteExperiments: function TPS_reloadRemoteExperiments(callback) {
|
||||||
|
for (let i = 0; i < this.taskList.length; i++) {
|
||||||
|
this.taskList[i].onExperimentShutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.taskList = [];
|
||||||
|
this._loader.unload();
|
||||||
|
|
||||||
|
this.checkForTasks(callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
getTaskById: function TPS_getTaskById(id) {
|
||||||
|
for (let i = 0; i < this.taskList.length; i++) {
|
||||||
|
let task = this.taskList[i];
|
||||||
|
if (task.id == id) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
getAllTasks: function TPS_getAllTasks() {
|
||||||
|
return this.taskList;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,56 @@
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is Test Pilot.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is Mozilla.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2007
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Jono X <jono@mozilla.com>
|
||||||
|
* Dan Mills <thunder@mozilla.com>
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||||||
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
EXPORTED_SYMBOLS = ["sanitizeString", "sanitizeJSONStrings"];
|
||||||
|
|
||||||
|
function sanitizeString(input) {
|
||||||
|
// Only allow alphanumerics, space, period, hypen, and underscore.
|
||||||
|
// Replace any other characters with question mark.
|
||||||
|
return input.replace(/[^a-zA-Z0-9 .\-_]/g, '?');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeJSONStrings(jsonBlob) {
|
||||||
|
// recursively goes through json and sanitizes every string it finds
|
||||||
|
for (let x in jsonBlob) {
|
||||||
|
if (typeof jsonBlob[x] == "string") {
|
||||||
|
jsonBlob[x] = sanitizeString(jsonBlob[x]);
|
||||||
|
} else if (typeof jsonBlob[x] == "object") {
|
||||||
|
jsonBlob[x] = sanitizeJSONStrings(jsonBlob[x]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonBlob;
|
||||||
|
}
|
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/badge-default.png
Normal file
После Ширина: | Высота: | Размер: 18 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/bg.jpg
Normal file
После Ширина: | Высота: | Размер: 96 KiB |
|
@ -0,0 +1,297 @@
|
||||||
|
html {
|
||||||
|
padding-bottom: 40px;
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Lucida Grande', 'Lucida Sans Unicode', Lucida, Arial, Helvetica, sans-serif;
|
||||||
|
background: #fff url('chrome://testpilot/skin/images/bg-status.jpg') repeat-x top center; padding: 0px;
|
||||||
|
color:#787878;
|
||||||
|
font-size:12px;
|
||||||
|
line-height: 18px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {color: #3a3a3a; font-size: 28px; font-weight: normal; letter-spacing: -1px}
|
||||||
|
h2 {color: #54717b; font-size: 24px; line-height: 24px;font-weight: normal; letter-spacing: -1px}
|
||||||
|
h3 {color: #54717b; font-size: 18px; line-height: 18px;font-weight: normal; letter-spacing: -1px}
|
||||||
|
|
||||||
|
p, ul {font-size: 12px;}
|
||||||
|
|
||||||
|
a:link, a:hover, a:active, a:focus, a:visited {color: #9f423b; text-decoration: none;}
|
||||||
|
a:hover {color: #9f423b; text-decoration: none;}
|
||||||
|
|
||||||
|
.bold {color:#3a3a3a;}
|
||||||
|
.address {margin-left: 20px;}
|
||||||
|
.inactive {color:#ccc;}
|
||||||
|
|
||||||
|
li {list-style-type: circle;}
|
||||||
|
|
||||||
|
.spacer {height: 40px;}
|
||||||
|
|
||||||
|
.center {text-align: center;}
|
||||||
|
|
||||||
|
.data {
|
||||||
|
|
||||||
|
background-color: #fff;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
border: 1px solid rgba(133, 153, 166, 0.2);
|
||||||
|
border-bottom: 4px solid rgba(133, 153, 166, 0.2);
|
||||||
|
-moz-border-bottom-colors:rgba(133, 153, 166, 0.3) rgba(133, 153, 166, 0.2) rgba(133, 153, 166, 0.2) rgba(133, 153, 166, 0.2);
|
||||||
|
padding: 6px;
|
||||||
|
-moz-box-shadow:
|
||||||
|
rgba(133, 153, 166, 0.4) 0px 1px 24px;
|
||||||
|
-webkit-box-shadow:
|
||||||
|
rgba(133, 153, 166, 0.4) 0px 1px 24px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataBox {
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 6px 20px 20px 20px;
|
||||||
|
-moz-border-radius: 0.5em;
|
||||||
|
-webkit-border-radius: 0.5em;
|
||||||
|
background: rgba(220, 240, 247, 0.8) url('chrome://testpilot/skin/images/callout.png') no-repeat top center;
|
||||||
|
//display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
margin: 0px auto;
|
||||||
|
width: 950px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#logo {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#contentWelcome {
|
||||||
|
margin-top: 380px;
|
||||||
|
padding: 8px 24px 24px 24px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-standalone {
|
||||||
|
margin-top: 120px;
|
||||||
|
padding: 8px 24px 24px 24px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#download {
|
||||||
|
width: 300px;
|
||||||
|
margin-left: 410px;
|
||||||
|
margin-top: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadButton {
|
||||||
|
margin-bottom: -10px;
|
||||||
|
margin-top: -2px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadH1 {color: #3a3a3a; font-size: 24px; font-weight: normal; letter-spacing: -1px; margin-right: 8px;}
|
||||||
|
|
||||||
|
.downloadH2 {color: #3a3a3a; font-size: 22px; font-weight: normal; letter-spacing: -1px; line-height: 28px; margin-left: 46px;}
|
||||||
|
|
||||||
|
#intro {
|
||||||
|
float: left;
|
||||||
|
width: 500px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#links {
|
||||||
|
float: right;
|
||||||
|
padding: 24px 0px;
|
||||||
|
margin-right: 0px;
|
||||||
|
margin-top: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
-moz-border-radius: 0.5em;
|
||||||
|
-webkit-border-radius: 0.5em;
|
||||||
|
-moz-box-shadow:
|
||||||
|
inset rgba(0, 0, 0, 0.2) 0 1px 1px,
|
||||||
|
inset rgba(255, 255, 255, 1) 0 3px 1px,
|
||||||
|
inset rgba(255, 255, 255, 0.3) 0 16px 0px,
|
||||||
|
inset rgba(0, 0, 0, 0.2) 0 -1px 1px,
|
||||||
|
inset rgba(0, 0, 0, 0.1) 0 -2px 1px,
|
||||||
|
rgba(255, 255, 255, 1) 0 1px,
|
||||||
|
rgba(133, 153, 166, 0.3) 0px 1px 12px;
|
||||||
|
background-color: #e7eaec;
|
||||||
|
//display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home_button {
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
width: 240px;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
-moz-border-radius: 0.5em;
|
||||||
|
-webkit-border-radius: 0.5em;
|
||||||
|
-moz-box-shadow:
|
||||||
|
inset rgba(0, 0, 0, 0.2) 0 1px 1px,
|
||||||
|
inset rgba(255, 255, 255, 1) 0 3px 1px,
|
||||||
|
inset rgba(255, 255, 255, 0.3) 0 16px 0px,
|
||||||
|
inset rgba(0, 0, 0, 0.2) 0 -1px 1px,
|
||||||
|
inset rgba(0, 0, 0, 0.1) 0 -2px 1px,
|
||||||
|
rgba(255, 255, 255, 1) 0 1px,
|
||||||
|
rgba(133, 153, 166, 0.3) 0px 1px 12px;
|
||||||
|
background-color: #e7eaec;
|
||||||
|
//display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout {
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 8px 24px;
|
||||||
|
margin: 24px auto;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
-moz-border-radius: 0.5em;
|
||||||
|
-webkit-border-radius: 0.5em;
|
||||||
|
background: rgba(220, 240, 247, 0.8) url('chrome://testpilot/skin/images/callout.png') no-repeat top center;
|
||||||
|
-moz-box-shadow:
|
||||||
|
inset rgba(185, 221, 234, 0.2) 0 -10px 12px,
|
||||||
|
inset rgba(185, 221, 234, 1) 0 0px 1px,
|
||||||
|
inset rgba(255, 255, 255, 0.2) 0 10px 12px;
|
||||||
|
//display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#data-privacy-text {
|
||||||
|
width: 320px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home_callout {
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 280px;
|
||||||
|
padding: 8px 24px;
|
||||||
|
margin: 8px auto;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
-moz-border-radius: 0.5em;
|
||||||
|
-webkit-border-radius: 0.5em;
|
||||||
|
background: rgba(220, 240, 247, 0.8) url('chrome://testpilot/skin/images/callout.png') no-repeat top center;
|
||||||
|
-moz-box-shadow:
|
||||||
|
inset rgba(185, 221, 234, 0.2) 0 -10px 12px,
|
||||||
|
inset rgba(185, 221, 234, 1) 0 0px 1px,
|
||||||
|
inset rgba(255, 255, 255, 0.2) 0 10px 12px;
|
||||||
|
//display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home_callout_continue {
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 280px;
|
||||||
|
padding: 8px 24px;
|
||||||
|
margin: 8px auto;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
-moz-border-radius: 0.5em;
|
||||||
|
-webkit-border-radius: 0.5em;
|
||||||
|
background: rgba(220, 240, 247, 0.8) url('chrome://testpilot/skin/images/callout_continue.png') no-repeat top center;
|
||||||
|
-moz-box-shadow:
|
||||||
|
inset rgba(185, 221, 234, 0.2) 0 -10px 12px,
|
||||||
|
inset rgba(185, 221, 234, 1) 0 0px 1px,
|
||||||
|
inset rgba(255, 255, 255, 0.2) 0 10px 12px;
|
||||||
|
//display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home_callout_continue a {color: #6c9735; text-decoration: none;}
|
||||||
|
.home_callout_continue a:hover {color: #6c9735; text-decoration: none; border-bottom: 1px dotted #6c9735;}
|
||||||
|
|
||||||
|
|
||||||
|
.homeIcon {
|
||||||
|
margin-top: -32px;
|
||||||
|
margin-bottom: -32px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.homeIconSubmit {
|
||||||
|
margin-top: -32px;
|
||||||
|
margin-bottom: -32px;
|
||||||
|
margin-right: 12px;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
clear: both;
|
||||||
|
padding: 24px;
|
||||||
|
font-size: 9px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.mozLogo {
|
||||||
|
margin-bottom: -10px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------- MENU -------
|
||||||
|
|
||||||
|
#menu {
|
||||||
|
margin: 20px auto;
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 4px 40px;
|
||||||
|
width: 800px;
|
||||||
|
text-align: left;
|
||||||
|
-moz-border-radius: 0.25em;
|
||||||
|
-webkit-border-radius: 0.25em;
|
||||||
|
border-top: 1px solid #adb6ba;
|
||||||
|
border-left: 1px solid #adb6ba;
|
||||||
|
border-right: 1px solid #adb6ba;
|
||||||
|
border-bottom: 3px solid #adb6ba;
|
||||||
|
-moz-border-bottom-colors:#adb6ba #e7eaec #e7eaec;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
.menuItem {
|
||||||
|
margin-right: 30px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-shadow: 1px 1px 1px rgba(173, 182, 186, 0.6);
|
||||||
|
padding: 9px 8px 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuOn {
|
||||||
|
margin-right: 30px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
text-shadow: 1px 1px 1px rgba(173, 182, 186, 1);
|
||||||
|
background-color: rgba(173, 182, 186, 0.3);
|
||||||
|
-moz-box-shadow:
|
||||||
|
inset rgba(0, 0, 0, 0.2) 0 -10px 12px;
|
||||||
|
padding: 9px 8px 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuItem a {color: #9f423b; text-decoration: none;}
|
||||||
|
.menuItem a:hover {color: #9f423b; text-decoration: none; border-bottom: 1px dotted #9f423b;}
|
||||||
|
|
||||||
|
|
||||||
|
.menuOn a {color: #666666; text-decoration: none;}
|
||||||
|
.menuOn a:hover {color: #666666; text-decoration: none;}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #9f423b;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #9f423b; text-decoration: underline; cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#survey-explanation {
|
||||||
|
color: #787878;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.survey-question-explanation {
|
||||||
|
color: #787878;
|
||||||
|
}
|
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/dino_32x32.png
Normal file
После Ширина: | Высота: | Размер: 1.5 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/bg-status.jpg
Normal file
После Ширина: | Высота: | Размер: 84 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/callout.png
Normal file
После Ширина: | Высота: | Размер: 18 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/callout_continue.png
Normal file
После Ширина: | Высота: | Размер: 15 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/data1.jpg
Normal file
После Ширина: | Высота: | Размер: 24 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/data2.jpg
Normal file
После Ширина: | Высота: | Размер: 24 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_comments.png
Normal file
После Ширина: | Высота: | Размер: 2.5 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_computer.png
Normal file
После Ширина: | Высота: | Размер: 4.2 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_continue.png
Normal file
После Ширина: | Высота: | Размер: 673 B |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_quit.png
Normal file
После Ширина: | Высота: | Размер: 961 B |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_results.png
Normal file
После Ширина: | Высота: | Размер: 3.4 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_twitter.png
Normal file
После Ширина: | Высота: | Размер: 2.6 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/images/home_upcoming.png
Normal file
После Ширина: | Высота: | Размер: 3.5 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/logo.png
Normal file
После Ширина: | Высота: | Размер: 2.0 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/mozilla-logo.png
Normal file
После Ширина: | Высота: | Размер: 949 B |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-completed.png
Normal file
После Ширина: | Высота: | Размер: 2.6 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-ejected.png
Normal file
После Ширина: | Высота: | Размер: 1.3 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/status-missed.png
Normal file
После Ширина: | Высота: | Размер: 1.6 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/testPilot_200x200.png
Normal file
После Ширина: | Высота: | Размер: 43 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/testpilot_16x16.png
Normal file
После Ширина: | Высота: | Размер: 489 B |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/testpilot_32x32.png
Normal file
После Ширина: | Высота: | Размер: 2.4 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-completedstudies-32x32.png
Normal file
После Ширина: | Высота: | Размер: 1.5 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-currentstudies-32x32.png
Normal file
После Ширина: | Высота: | Размер: 1.0 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-generic-32x32.png
Normal file
После Ширина: | Высота: | Размер: 1.5 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-learned-32x32.png
Normal file
После Ширина: | Высота: | Размер: 1.2 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-results-48x48.png
Normal file
После Ширина: | Высота: | Размер: 2.6 KiB |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-settings-32x32.png
Normal file
После Ширина: | Высота: | Размер: 967 B |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-study-48x48.png
Normal file
После Ширина: | Высота: | Размер: 962 B |
Двоичные данные
browser/app/profile/extensions/testpilot@labs.mozilla.com/skin/all/tp-submit-48x48.png
Normal file
После Ширина: | Высота: | Размер: 2.6 KiB |