Auto-merge with m-c b51803f3fdef

This commit is contained in:
Oleg Romashin 2010-06-25 08:30:40 -04:00
commit 39a0145aea
187 changed files with 19949 additions and 327 deletions

View File

@ -45,3 +45,17 @@ include $(DEPTH)/config/autoconf.mk
DIRS = {972ce4c6-7e08-4474-a285-3208198ce6fd}
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

View File

@ -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

View File

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

View File

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

View File

@ -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>

View File

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

View File

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

View File

@ -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>

View File

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

View File

@ -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>

View File

@ -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]
};
})();

View 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]}})();

View File

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

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

View File

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

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

File diff suppressed because one or more lines are too long

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

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

View File

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

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

View File

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

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

View File

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

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

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

View File

@ -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>

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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 + "&nbsp:&nbsp";
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 + "&nbsp:&nbsp";
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);
}
}

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -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>&nbsp;
<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>&nbsp; &nbsp;
<a href="http://www.mozilla.com/en-US/privacy-policy.html"
id="privacy-policy-link"></a> &nbsp; &nbsp;
<a href="http://www.mozilla.com/en-US/about/legal.html"
id="legal-notices-link"></a>
</div>
</div>
</body></html>

View File

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

View File

@ -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");

View File

@ -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>

View File

@ -0,0 +1,3 @@
resource instrument .
content instrument .
overlay chrome://browser/content chrome://instrument/content

View File

@ -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>

View File

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

View File

@ -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>

View File

@ -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() {}
};

View File

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

View File

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

View File

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

View File

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

View File

@ -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;

View File

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

View File

@ -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 = {}; });

View File

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

View File

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

View File

@ -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.
}
};

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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.
};

View File

@ -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.

View File

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

View File

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

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Some files were not shown because too many files have changed in this diff Show More