mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
83a9d7cc56
--HG-- extra : transplant_source : e%C3%0EN%E61%15%84%08%97%0E%91%FD%14%B3%92J%B1%84%97
1111 lines
38 KiB
JavaScript
1111 lines
38 KiB
JavaScript
/* ***** 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 = ["TaskConstants", "TestPilotBuiltinSurvey",
|
|
"TestPilotExperiment", "TestPilotStudyResults",
|
|
"TestPilotLegacyStudy", "TestPilotWebSurvey"];
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
|
|
Components.utils.import("resource://testpilot/modules/Observers.js");
|
|
Components.utils.import("resource://testpilot/modules/metadata.js");
|
|
Components.utils.import("resource://testpilot/modules/log4moz.js");
|
|
Components.utils.import("resource://testpilot/modules/string_sanitizer.js");
|
|
|
|
const STATUS_PREF_PREFIX = "extensions.testpilot.taskstatus.";
|
|
const START_DATE_PREF_PREFIX = "extensions.testpilot.startDate.";
|
|
const RECUR_PREF_PREFIX = "extensions.testpilot.reSubmit.";
|
|
const RECUR_TIMES_PREF_PREFIX = "extensions.testpilot.recurCount.";
|
|
const SURVEY_ANSWER_PREFIX = "extensions.testpilot.surveyAnswers.";
|
|
const EXPIRATION_DATE_FOR_DATA_SUBMISSION_PREFIX =
|
|
"extensions.testpilot.expirationDateForDataSubmission.";
|
|
const DATE_FOR_DATA_DELETION_PREFIX =
|
|
"extensions.testpilot.dateForDataDeletion.";
|
|
const GUID_PREF_PREFIX = "extensions.testpilot.taskGUID.";
|
|
const RETRY_INTERVAL_PREF = "extensions.testpilot.uploadRetryInterval";
|
|
const TIME_FOR_DATA_DELETION = 7 * (24 * 60 * 60 * 1000); // 7 days
|
|
const DATA_UPLOAD_PREF = "extensions.testpilot.dataUploadURL";
|
|
const DEFAULT_THUMBNAIL_URL = "chrome://testpilot/skin/badge-default.png";
|
|
|
|
|
|
const TaskConstants = {
|
|
// TODO status RESULTS and ARCHIVED don't make sense for studies anymore;
|
|
// we need a status MISSED and a status EXPIRED. (can you be cancelled and
|
|
// expired?)
|
|
STATUS_NEW: 0, // It's new and you haven't seen it yet.
|
|
STATUS_PENDING : 1, // You've seen it but it hasn't started.
|
|
STATUS_STARTING: 2, // Data collection started but notification not shown.
|
|
STATUS_IN_PROGRESS : 3, // Started and notification shown.
|
|
STATUS_FINISHED : 4, // Finished and awaiting your choice.
|
|
STATUS_CANCELLED : 5, // You've opted out and not submitted anything.
|
|
STATUS_SUBMITTED : 6, // You've submitted your data.
|
|
STATUS_RESULTS : 7, // Test finished AND final results visible somewhere, deprecated
|
|
STATUS_ARCHIVED: 8, // Results seen. Deprecated. TODO: or use for expired?
|
|
STATUS_MISSED: 9, // You never ran this study.
|
|
|
|
TYPE_EXPERIMENT : 1,
|
|
TYPE_SURVEY : 2,
|
|
TYPE_RESULTS : 3,
|
|
TYPE_LEGACY: 4,
|
|
|
|
ALWAYS_SUBMIT: 1,
|
|
NEVER_SUBMIT: -1,
|
|
ASK_EACH_TIME: 0
|
|
};
|
|
/* Note that experiments use all 9 status codes, but surveys don't have a
|
|
* data collection period so they are never STARTING or IN_PROGRESS or
|
|
* FINISHED, they go straight from PENDING to SUBMITTED or CANCELED. */
|
|
|
|
let Application = Cc["@mozilla.org/fuel/application;1"]
|
|
.getService(Ci.fuelIApplication);
|
|
|
|
// Prototype for both TestPilotSurvey and TestPilotExperiment.
|
|
var TestPilotTask = {
|
|
_id: null,
|
|
_title: null,
|
|
_status: null,
|
|
_url: null,
|
|
|
|
_taskInit: function TestPilotTask__taskInit(id, title, url, summary, thumb) {
|
|
this._id = id;
|
|
this._title = title;
|
|
this._status = Application.prefs.getValue(STATUS_PREF_PREFIX + this._id,
|
|
TaskConstants.STATUS_NEW);
|
|
this._url = url;
|
|
this._summary = summary;
|
|
this._thumbnail = thumb;
|
|
this._logger = Log4Moz.repository.getLogger("TestPilot.Task_"+this._id);
|
|
},
|
|
|
|
get title() {
|
|
return this._title;
|
|
},
|
|
|
|
get id() {
|
|
return this._id;
|
|
},
|
|
|
|
get taskType() {
|
|
return null;
|
|
},
|
|
|
|
get status() {
|
|
return this._status;
|
|
},
|
|
|
|
get webContent() {
|
|
return this._webContent;
|
|
},
|
|
|
|
get summary() {
|
|
if (this._summary) {
|
|
return this._summary;
|
|
} else {
|
|
return this._title;
|
|
}
|
|
},
|
|
|
|
get thumbnail() {
|
|
if (this._thumbnail) {
|
|
return this._thumbnail;
|
|
} else {
|
|
return DEFAULT_THUMBNAIL_URL;
|
|
}
|
|
},
|
|
|
|
// urls:
|
|
get infoPageUrl() {
|
|
return this._url;
|
|
},
|
|
|
|
get currentStatusUrl() {
|
|
return this._url;
|
|
},
|
|
|
|
get defaultUrl() {
|
|
return this.infoPageUrl;
|
|
},
|
|
|
|
get uploadUrl() {
|
|
let url = Application.prefs.getValue(DATA_UPLOAD_PREF, "");
|
|
return url + this._id;
|
|
},
|
|
|
|
get showMoreInfoLink() {
|
|
return true;
|
|
},
|
|
|
|
// event handlers:
|
|
|
|
onExperimentStartup: function TestPilotTask_onExperimentStartup() {
|
|
},
|
|
|
|
onExperimentShutdown: function TestPilotTask_onExperimentShutdown() {
|
|
},
|
|
|
|
onAppStartup: function TestPilotTask_onAppStartup() {
|
|
// Called by extension core when startup is complete.
|
|
},
|
|
|
|
onAppShutdown: function TestPilotTask_onAppShutdown() {
|
|
// TODO: not implemented - should be called when firefox is ready to
|
|
// shut down.
|
|
},
|
|
|
|
onEnterPrivateBrowsing: function TestPilotTask_onEnterPrivate() {
|
|
},
|
|
|
|
onExitPrivateBrowsing: function TestPilotTask_onExitPrivate() {
|
|
},
|
|
|
|
onNewWindow: function TestPilotTask_onNewWindow(window) {
|
|
},
|
|
|
|
onWindowClosed: function TestPilotTask_onWindowClosed(window) {
|
|
},
|
|
|
|
onUrlLoad: function TestPilotTask_onUrlLoad(url) {
|
|
},
|
|
|
|
onDetailPageOpened: function TestPilotTask_onDetailPageOpened(){
|
|
// TODO fold this into loadPage()?
|
|
},
|
|
|
|
checkDate: function TestPilotTask_checkDate() {
|
|
},
|
|
|
|
changeStatus: function TPS_changeStatus(newStatus, suppressNotification) {
|
|
// TODO we always suppress notifications except when new status is
|
|
// "finished"; maybe remove that argument and only fire notification
|
|
// when status is "finished".
|
|
let logger = Log4Moz.repository.getLogger("TestPilot.Task");
|
|
logger.info("Changing task " + this._id + " status to " + newStatus);
|
|
this._status = newStatus;
|
|
// Set the pref:
|
|
Application.prefs.setValue(STATUS_PREF_PREFIX + this._id, newStatus);
|
|
// Notify user of status change:
|
|
if (!suppressNotification) {
|
|
Observers.notify("testpilot:task:changed", "", null);
|
|
}
|
|
},
|
|
|
|
loadPage: function TestPilotTask_loadPage() {
|
|
// open the link in the chromeless window
|
|
var wm = Cc["@mozilla.org/appshell/window-mediator;1"]
|
|
.getService(Ci.nsIWindowMediator);
|
|
let window = wm.getMostRecentWindow("navigator:browser");
|
|
window.TestPilotWindowUtils.openChromeless(this.defaultUrl);
|
|
/* Advance the status when the user sees the page, so that we can stop
|
|
* notifying them about stuff they've seen. */
|
|
if (this._status == TaskConstants.STATUS_NEW) {
|
|
this.changeStatus(TaskConstants.STATUS_PENDING);
|
|
} else if (this._status == TaskConstants.STATUS_STARTING) {
|
|
this.changeStatus(TaskConstants.STATUS_IN_PROGRESS);
|
|
} else if (this._status == TaskConstants.STATUS_RESULTS) {
|
|
this.changeStatus(TaskConstants.STATUS_ARCHIVED);
|
|
}
|
|
|
|
this.onDetailPageOpened();
|
|
}
|
|
};
|
|
|
|
function TestPilotExperiment(expInfo, dataStore, handlers, webContent) {
|
|
// All four of these are objects defined in the remote experiment file
|
|
this._init(expInfo, dataStore, handlers, webContent);
|
|
}
|
|
TestPilotExperiment.prototype = {
|
|
_init: function TestPilotExperiment__init(expInfo,
|
|
dataStore,
|
|
handlers,
|
|
webContent) {
|
|
/* expInfo is a dictionary defined in the remote experiment code, which
|
|
* should have the following properties:
|
|
* startDate (string representation of date)
|
|
* duration (number of days)
|
|
* testName (human-readable string)
|
|
* testId (int)
|
|
* testInfoUrl (url)
|
|
* summary (string - couple of sentences explaining study)
|
|
* thumbnail (url of an image representing the study)
|
|
* optInRequired (boolean)
|
|
* recursAutomatically (boolean)
|
|
* recurrenceInterval (number of days)
|
|
* versionNumber (int) */
|
|
this._taskInit(expInfo.testId, expInfo.testName, expInfo.testInfoUrl,
|
|
expInfo.summary, expInfo.thumbnail);
|
|
this._webContent = webContent;
|
|
this._dataStore = dataStore;
|
|
this._versionNumber = expInfo.versionNumber;
|
|
this._optInRequired = expInfo.optInRequired;
|
|
// TODO implement opt-in interface for tests that require opt-in.
|
|
this._recursAutomatically = expInfo.recursAutomatically;
|
|
this._recurrenceInterval = expInfo.recurrenceInterval;
|
|
|
|
let prefName = START_DATE_PREF_PREFIX + this._id;
|
|
let startDateString = Application.prefs.getValue(prefName, false);
|
|
if (startDateString) {
|
|
// If this isn't the first time we're starting, use the start date
|
|
// already stored in prefs.
|
|
this._startDate = Date.parse(startDateString);
|
|
} else {
|
|
// If a start date is provided in expInfo, use that.
|
|
// Otherwise, start immediately!
|
|
if (expInfo.startDate) {
|
|
this._startDate = Date.parse(expInfo.startDate);
|
|
Application.prefs.setValue(prefName, expInfo.startDate);
|
|
} else {
|
|
this._startDate = Date.now();
|
|
Application.prefs.setValue(prefName, (new Date()).toString());
|
|
}
|
|
}
|
|
|
|
// Duration is specified in days:
|
|
let duration = expInfo.duration || 7; // default 1 week
|
|
this._endDate = this._startDate + duration * (24 * 60 * 60 * 1000);
|
|
this._logger.info("Start date is " + this._startDate.toString());
|
|
this._logger.info("End date is " + this._endDate.toString());
|
|
|
|
this._handlers = handlers;
|
|
this._uploadRetryTimer = null;
|
|
this._startedUpHandlers = false;
|
|
|
|
// checkDate will see what our status is with regards to the start and
|
|
// end dates, and set status appropriately.
|
|
this.checkDate();
|
|
|
|
if (this.experimentIsRunning) {
|
|
this.onExperimentStartup();
|
|
}
|
|
},
|
|
|
|
get taskType() {
|
|
return TaskConstants.TYPE_EXPERIMENT;
|
|
},
|
|
|
|
get endDate() {
|
|
return this._endDate;
|
|
},
|
|
|
|
get startDate() {
|
|
return this._startDate;
|
|
},
|
|
|
|
get dataStore() {
|
|
return this._dataStore;
|
|
},
|
|
|
|
get currentStatusUrl() {
|
|
let param = "?eid=" + this._id;
|
|
return "chrome://testpilot/content/status.html" + param;
|
|
},
|
|
|
|
get defaultUrl() {
|
|
return this.currentStatusUrl;
|
|
},
|
|
|
|
get recurPref() {
|
|
let prefName = RECUR_PREF_PREFIX + this._id;
|
|
return Application.prefs.getValue(prefName, TaskConstants.ASK_EACH_TIME);
|
|
},
|
|
|
|
getDataStoreAsJSON: function(callback) {
|
|
this._dataStore.getAllDataAsJSON(false, callback);
|
|
},
|
|
|
|
getWebContent: function TestPilotExperiment_getWebContent(callback) {
|
|
let content = "";
|
|
let waitForData = false;
|
|
let self = this;
|
|
|
|
switch (this._status) {
|
|
case TaskConstants.STATUS_NEW:
|
|
case TaskConstants.STATUS_PENDING:
|
|
content = this.webContent.upcomingHtml;
|
|
break;
|
|
case TaskConstants.STATUS_STARTING:
|
|
case TaskConstants.STATUS_IN_PROGRESS:
|
|
content = this.webContent.inProgressHtml;
|
|
break;
|
|
case TaskConstants.STATUS_FINISHED:
|
|
waitForData = true;
|
|
this._dataStore.haveData(function(withData) {
|
|
if (withData) {
|
|
content = self.webContent.completedHtml;
|
|
} else {
|
|
// for after deleting data manually by user.
|
|
let stringBundle =
|
|
Components.classes["@mozilla.org/intl/stringbundle;1"].
|
|
getService(Components.interfaces.nsIStringBundleService).
|
|
createBundle("chrome://testpilot/locale/main.properties");
|
|
let link =
|
|
'<a href="' + this.infoPageUrl + '">"' + this.title +
|
|
'"</a>';
|
|
content =
|
|
'<h2>' + stringBundle.formatStringFromName(
|
|
"testpilot.finishedTask.finishedStudy", [link], 1) + '</h2>' +
|
|
'<p>' + stringBundle.GetStringFromName(
|
|
"testpilot.finishedTask.allRelatedDataDeleted") + '</p>';
|
|
}
|
|
callback(content);
|
|
});
|
|
break;
|
|
case TaskConstants.STATUS_CANCELLED:
|
|
if (this._expirationDateForDataSubmission.length == 0) {
|
|
content = this.webContent.canceledHtml;
|
|
} else {
|
|
content = this.webContent.dataExpiredHtml;
|
|
}
|
|
break;
|
|
case TaskConstants.STATUS_SUBMITTED:
|
|
if (this._dateForDataDeletion.length > 0) {
|
|
content = this.webContent.remainDataHtml;
|
|
} else {
|
|
content = this.webContent.deletedRemainDataHtml;
|
|
}
|
|
break;
|
|
}
|
|
// TODO what to do if status is cancelled, submitted, results, or archived?
|
|
if (!waitForData) {
|
|
callback(content);
|
|
}
|
|
},
|
|
|
|
getDataPrivacyContent: function(callback) {
|
|
let content = "";
|
|
let waitForData = false;
|
|
let self = this;
|
|
|
|
switch (this._status) {
|
|
case TaskConstants.STATUS_STARTING:
|
|
case TaskConstants.STATUS_IN_PROGRESS:
|
|
content = this.webContent.inProgressDataPrivacyHtml;
|
|
break;
|
|
case TaskConstants.STATUS_FINISHED:
|
|
waitForData = true;
|
|
this._dataStore.haveData(function(withData) {
|
|
if (withData) {
|
|
content = self.webContent.completedDataPrivacyHtml;
|
|
}
|
|
callback(content);
|
|
});
|
|
break;
|
|
case TaskConstants.STATUS_CANCELLED:
|
|
if (this._expirationDateForDataSubmission.length == 0) {
|
|
content = this.webContent.canceledDataPrivacyHtml;
|
|
} else {
|
|
content = this.webContent.dataExpiredDataPrivacyHtml;
|
|
}
|
|
break;
|
|
case TaskConstants.STATUS_SUBMITTED:
|
|
if (this._dateForDataDeletion.length > 0) {
|
|
content = this.webContent.remainDataDataPrivacyHtml;
|
|
} else {
|
|
content = this.webContent.deletedRemainDataDataPrivacyHtml;
|
|
}
|
|
break;
|
|
}
|
|
if (!waitForData) {
|
|
callback(content);
|
|
}
|
|
},
|
|
|
|
experimentIsRunning: function TestPilotExperiment_isRunning() {
|
|
// bug 575767
|
|
return (this._status == TaskConstants.STATUS_STARTING ||
|
|
this._status == TaskConstants.STATUS_IN_PROGRESS);
|
|
},
|
|
|
|
// Pass events along to handlers:
|
|
onNewWindow: function TestPilotExperiment_onNewWindow(window) {
|
|
this._logger.trace("Experiment.onNewWindow called.");
|
|
if (this.experimentIsRunning()) {
|
|
this._handlers.onNewWindow(window);
|
|
}
|
|
},
|
|
|
|
onWindowClosed: function TestPilotExperiment_onWindowClosed(window) {
|
|
this._logger.trace("Experiment.onWindowClosed called.");
|
|
if (this.experimentIsRunning()) {
|
|
this._handlers.onWindowClosed(window);
|
|
}
|
|
},
|
|
|
|
onAppStartup: function TestPilotExperiment_onAppStartup() {
|
|
this._logger.trace("Experiment.onAppStartup called.");
|
|
if (this.experimentIsRunning()) {
|
|
this._handlers.onAppStartup();
|
|
}
|
|
},
|
|
|
|
onAppShutdown: function TestPilotExperiment_onAppShutdown() {
|
|
this._logger.trace("Experiment.onAppShutdown called.");
|
|
// TODO the caller for this is not yet implemented
|
|
if (this.experimentIsRunning()) {
|
|
this._handlers.onAppShutdown();
|
|
}
|
|
},
|
|
|
|
onExperimentStartup: function TestPilotExperiment_onStartup() {
|
|
this._logger.trace("Experiment.onExperimentStartup called.");
|
|
// Make sure not to call this if it's already been called:
|
|
if (this.experimentIsRunning() && !this._startedUpHandlers) {
|
|
this._logger.trace(" ... starting up handlers!");
|
|
this._handlers.onExperimentStartup(this._dataStore);
|
|
this._startedUpHandlers = true;
|
|
}
|
|
},
|
|
|
|
onExperimentShutdown: function TestPilotExperiment_onShutdown() {
|
|
this._logger.trace("Experiment.onExperimentShutdown called.");
|
|
if (this.experimentIsRunning() && this._startedUpHandlers) {
|
|
this._handlers.onExperimentShutdown();
|
|
this._startedUpHandlers = false;
|
|
}
|
|
},
|
|
|
|
onEnterPrivateBrowsing: function TestPilotExperiment_onEnterPrivate() {
|
|
this._logger.trace("Task is entering private browsing.");
|
|
if (this.experimentIsRunning()) {
|
|
this._handlers.onEnterPrivateBrowsing();
|
|
}
|
|
},
|
|
|
|
onExitPrivateBrowsing: function TestPilotExperiment_onExitPrivate() {
|
|
this._logger.trace("Task is exiting private browsing.");
|
|
if (this.experimentIsRunning()) {
|
|
this._handlers.onExitPrivateBrowsing();
|
|
}
|
|
},
|
|
|
|
_reschedule: function TestPilotExperiment_reschedule() {
|
|
// Schedule next run of test:
|
|
// add recurrence interval to start date and store!
|
|
let ms = this._recurrenceInterval * (24 * 60 * 60 * 1000);
|
|
// recurrenceInterval is in days, convert to milliseconds:
|
|
this._startDate += ms;
|
|
this._endDate += ms;
|
|
let prefName = START_DATE_PREF_PREFIX + this._id;
|
|
Application.prefs.setValue(prefName,
|
|
(new Date(this._startDate)).toString());
|
|
},
|
|
|
|
get _numTimesRun() {
|
|
// For automatically recurring tests, this is the number of times it
|
|
// has recurred - it will be 1 on the first run, 2 on the second run,
|
|
// etc.
|
|
if (this._recursAutomatically) {
|
|
return Application.prefs.getValue(RECUR_TIMES_PREF_PREFIX + this._id,
|
|
1);
|
|
} else {
|
|
return 0;
|
|
}
|
|
},
|
|
|
|
set _expirationDateForDataSubmission(date) {
|
|
if (date) {
|
|
Application.prefs.setValue(
|
|
EXPIRATION_DATE_FOR_DATA_SUBMISSION_PREFIX + this._id,
|
|
(new Date(date)).toString());
|
|
} else {
|
|
Application.prefs.setValue(
|
|
EXPIRATION_DATE_FOR_DATA_SUBMISSION_PREFIX + this._id, "");
|
|
}
|
|
},
|
|
|
|
get _expirationDateForDataSubmission() {
|
|
return Application.prefs.getValue(
|
|
EXPIRATION_DATE_FOR_DATA_SUBMISSION_PREFIX + this._id, "");
|
|
},
|
|
|
|
set _dateForDataDeletion(date) {
|
|
if (date) {
|
|
Application.prefs.setValue(
|
|
DATE_FOR_DATA_DELETION_PREFIX + this._id, (new Date(date)).toString());
|
|
} else {
|
|
Application.prefs.setValue(DATE_FOR_DATA_DELETION_PREFIX + this._id, "");
|
|
}
|
|
},
|
|
|
|
get _dateForDataDeletion() {
|
|
return Application.prefs.getValue(
|
|
DATE_FOR_DATA_DELETION_PREFIX + this._id, "");
|
|
},
|
|
|
|
checkDate: function TestPilotExperiment_checkDate() {
|
|
// This method handles all date-related status changes and should be
|
|
// called periodically.
|
|
let currentDate = Date.now();
|
|
|
|
// Reset automatically recurring tests:
|
|
if (this._recursAutomatically &&
|
|
this._status >= TaskConstants.STATUS_FINISHED &&
|
|
currentDate >= this._startDate &&
|
|
currentDate <= this._endDate) {
|
|
// if we've done a permanent opt-out, then don't start over-
|
|
// just keep rescheduling.
|
|
if (this.recurPref == TaskConstants.NEVER_SUBMIT) {
|
|
this._logger.info("recurPref is never submit, so I'm rescheduling.");
|
|
this._reschedule();
|
|
} else {
|
|
// Normal case is reset to new.
|
|
this.changeStatus(TaskConstants.STATUS_NEW, true);
|
|
|
|
// increment count of how many times this recurring test has run
|
|
let numTimesRun = this._numTimesRun;
|
|
numTimesRun++;
|
|
this._logger.trace("Test recurring... incrementing " + RECUR_TIMES_PREF_PREFIX + this._id + " to " + numTimesRun);
|
|
Application.prefs.setValue( RECUR_TIMES_PREF_PREFIX + this._id,
|
|
numTimesRun );
|
|
this._logger.trace("Incremented it.");
|
|
}
|
|
}
|
|
|
|
// If the notify-on-new-study pref is turned off, and the test doesn't
|
|
// require opt-in, then it can jump straight ahead to STARTING.
|
|
if (!this._optInRequired &&
|
|
!Application.prefs.getValue("extensions.testpilot.popup.showOnNewStudy",
|
|
false) &&
|
|
(this._status == TaskConstants.STATUS_NEW ||
|
|
this._status == TaskConstants.STATUS_PENDING)) {
|
|
this._logger.info("Skipping pending and going straight to starting.");
|
|
this.changeStatus(TaskConstants.STATUS_STARTING, true);
|
|
}
|
|
|
|
// If a study is STARTING, and we're in the right date range,
|
|
// then start it, and move it to IN_PROGRESS.
|
|
if ( this._status == TaskConstants.STATUS_STARTING &&
|
|
currentDate >= this._startDate &&
|
|
currentDate <= this._endDate) {
|
|
this._logger.info("Study now starting.");
|
|
let uuidGenerator =
|
|
Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
|
|
let uuid = uuidGenerator.generateUUID().toString();
|
|
// remove the brackets from the generated UUID
|
|
if (uuid.indexOf("{") == 0) {
|
|
uuid = uuid.substring(1, (uuid.length - 1));
|
|
}
|
|
// clear the data before starting.
|
|
this._dataStore.wipeAllData();
|
|
this.changeStatus(TaskConstants.STATUS_STARTING, true);
|
|
Application.prefs.setValue(GUID_PREF_PREFIX + this._id, uuid);
|
|
this.onExperimentStartup();
|
|
}
|
|
|
|
// What happens when a test finishes:
|
|
if (this._status < TaskConstants.STATUS_FINISHED &&
|
|
currentDate > this._endDate) {
|
|
let self = this;
|
|
let setDataDeletionDate = true;
|
|
this._logger.info("Passed End Date - Switched Task Status to Finished");
|
|
this.changeStatus(TaskConstants.STATUS_FINISHED);
|
|
this.onExperimentShutdown();
|
|
|
|
if (this._recursAutomatically) {
|
|
this._reschedule();
|
|
// A recurring experiment may have been set to automatically submit. If
|
|
// so, submit now!
|
|
if (this.recurPref == TaskConstants.ALWAYS_SUBMIT) {
|
|
this._logger.info("Automatically Uploading Data");
|
|
this.upload(function(success) {
|
|
Observers.notify("testpilot:task:dataAutoSubmitted", self, null);
|
|
});
|
|
} else if (this.recurPref == TaskConstants.NEVER_SUBMIT) {
|
|
this._logger.info("Automatically opting out of uploading data");
|
|
this.changeStatus(TaskConstants.STATUS_CANCELLED, true);
|
|
this._dataStore.wipeAllData();
|
|
setDataDeletionDate = false;
|
|
} else {
|
|
if (Application.prefs.getValue(
|
|
"extensions.testpilot.alwaysSubmitData", false)) {
|
|
this.upload(function(success) {
|
|
if (success) {
|
|
Observers.notify(
|
|
"testpilot:task:dataAutoSubmitted", self, null);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
if (Application.prefs.getValue(
|
|
"extensions.testpilot.alwaysSubmitData", false)) {
|
|
this.upload(function(success) {
|
|
if (success) {
|
|
Observers.notify("testpilot:task:dataAutoSubmitted", self, null);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
if (setDataDeletionDate) {
|
|
let date = this._endDate + TIME_FOR_DATA_DELETION;
|
|
this._dateForDataDeletion = date;
|
|
this._expirationDateForDataSubmission = date;
|
|
} else {
|
|
this._dateForDataDeletion = null;
|
|
this._expirationDateForDataSubmission = null;
|
|
}
|
|
} else {
|
|
// only do this if the state is already finished and the data is expired.
|
|
if (this._status == TaskConstants.STATUS_FINISHED) {
|
|
if (Application.prefs.getValue(
|
|
"extensions.testpilot.alwaysSubmitData", false)) {
|
|
this.upload(function(success) {
|
|
if (success) {
|
|
Observers.notify("testpilot:task:dataAutoSubmitted", self, null);
|
|
}
|
|
});
|
|
} else if (this._expirationDateForDataSubmission.length > 0) {
|
|
let expirationDate = Date.parse(this._expirationDateForDataSubmission);
|
|
if (currentDate > expirationDate) {
|
|
this.changeStatus(TaskConstants.STATUS_CANCELLED, true);
|
|
this._dataStore.wipeAllData();
|
|
this._dateForDataDeletion = null;
|
|
// need to keep the expirationDateForDataSubmission value so
|
|
// we know that data is expired, not user cancels the task.
|
|
}
|
|
}
|
|
} else if (this._status == TaskConstants.STATUS_SUBMITTED) {
|
|
if (this._dateForDataDeletion.length > 0) {
|
|
let deleteDate = Date.parse(this._dateForDataDeletion);
|
|
if (currentDate > deleteDate) {
|
|
this._dataStore.wipeAllData();
|
|
this._dateForDataDeletion = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
_prependMetadataToJSON: function TestPilotExperiment__prependToJson(callback) {
|
|
let json = {};
|
|
let self = this;
|
|
MetadataCollector.getMetadata(function(md) {
|
|
json.metadata = md;
|
|
let guid = Application.prefs.getValue(GUID_PREF_PREFIX + self._id, "");
|
|
json.metadata.task_guid = guid;
|
|
json.metadata.event_headers = self._dataStore.getPropertyNames();
|
|
self._dataStore.getJSONRows(function(rows) {
|
|
json.events = rows;
|
|
callback( JSON.stringify(json) );
|
|
});
|
|
});
|
|
},
|
|
|
|
// Note: When we have multiple experiments running, the uploads
|
|
// are separate files.
|
|
upload: function TestPilotExperiment_upload(callback, retryCount) {
|
|
// Callback is a function that will be called back with true or false
|
|
// on success or failure.
|
|
|
|
/* If we've already uploaded, and the user tries to upload again for
|
|
* some reason (they could navigate back to the status.html page,
|
|
* for instance), then proceed without uploading: */
|
|
if (this._status >= TaskConstants.STATUS_SUBMITTED) {
|
|
callback(true);
|
|
return;
|
|
}
|
|
|
|
// note the server will reject any upload over 5MB - shouldn't be a problem
|
|
let self = this;
|
|
let url = self.uploadUrl;
|
|
self._logger.info("Posting data to url " + url + "\n");
|
|
self._prependMetadataToJSON( function(dataString) {
|
|
let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
.createInstance( Ci.nsIXMLHttpRequest );
|
|
req.open('POST', url, true);
|
|
req.setRequestHeader("Content-type", "application/json");
|
|
req.setRequestHeader("Content-length", dataString.length);
|
|
req.setRequestHeader("Connection", "close");
|
|
req.onreadystatechange = function(aEvt) {
|
|
if (req.readyState == 4) {
|
|
if (req.status == 201) {
|
|
let location = req.getResponseHeader("Location");
|
|
self._logger.info("DATA WAS POSTED SUCCESSFULLY " + location);
|
|
if (self._uploadRetryTimer) {
|
|
self._uploadRetryTimer.cancel(); // Stop retrying - it worked!
|
|
}
|
|
self.changeStatus(TaskConstants.STATUS_SUBMITTED);
|
|
self._dateForDataDeletion = Date.now() + TIME_FOR_DATA_DELETION;
|
|
self._expirationDateForDataSubmission = null;
|
|
callback(true);
|
|
} else {
|
|
/* If something went wrong with the upload, give up for now,
|
|
* but start a timer to try again later. Maybe the network will
|
|
* be better by then. "later" starts at 1 hour, but increases
|
|
* using a random exponential function. This serves as a backoff
|
|
* in cases where a lot of users are trying to submit data at
|
|
* the same time and the network or server can't handle it.
|
|
*/
|
|
|
|
// TODO don't retry if status code is 401, 404, or...
|
|
// any others?
|
|
self._logger.warn("ERROR POSTING DATA: " + req.responseText);
|
|
self._uploadRetryTimer = Cc["@mozilla.org/timer;1"]
|
|
.createInstance(Ci.nsITimer);
|
|
|
|
if (!retryCount) {
|
|
retryCount = 0;
|
|
}
|
|
let interval =
|
|
Application.prefs.getValue(RETRY_INTERVAL_PREF, 3600000); // 1 hour
|
|
let delay =
|
|
parseInt(Math.random() * Math.pow(2, retryCount) * interval);
|
|
self._uploadRetryTimer.initWithCallback(
|
|
{ notify: function(timer) {
|
|
self.upload(callback, retryCount++);
|
|
} }, (interval + delay), Ci.nsITimer.TYPE_ONE_SHOT);
|
|
callback(false);
|
|
}
|
|
}
|
|
};
|
|
req.send(dataString);
|
|
});
|
|
},
|
|
|
|
optOut: function TestPilotExperiment_optOut(reason, callback) {
|
|
// Regardless of study ID, post the opt-out message to a special
|
|
// database table of just opt-out messages; include study ID in metadata.
|
|
let url = Application.prefs.getValue(DATA_UPLOAD_PREF, "") + "opt-out";
|
|
let logger = this._logger;
|
|
this.changeStatus(TaskConstants.STATUS_CANCELLED);
|
|
this._dataStore.wipeAllData();
|
|
this._dateForDataDeletion = null;
|
|
this._expirationDateForDataSubmission = null;
|
|
logger.info("Opting out of test with reason " + reason);
|
|
if (reason) {
|
|
// Send us the reason...
|
|
// (TODO: include metadata?)
|
|
let answer = {id: this._id,
|
|
reason: reason};
|
|
let dataString = JSON.stringify(answer);
|
|
var req =
|
|
Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
|
|
createInstance(Ci.nsIXMLHttpRequest);
|
|
logger.trace("Posting " + dataString + " to " + url);
|
|
req.open('POST', url, true);
|
|
req.setRequestHeader("Content-type", "application/json");
|
|
req.setRequestHeader("Content-length", dataString.length);
|
|
req.setRequestHeader("Connection", "close");
|
|
req.onreadystatechange = function(aEvt) {
|
|
if (req.readyState == 4) {
|
|
if (req.status == 200) {
|
|
logger.info("Quit reason posted successfully " + req.responseText);
|
|
callback(true);
|
|
} else {
|
|
logger.warn(req.status + " posting error " + req.responseText);
|
|
callback(false);
|
|
}
|
|
}
|
|
};
|
|
logger.trace("Sending quit reason.");
|
|
req.send(dataString);
|
|
} else {
|
|
callback(false);
|
|
}
|
|
},
|
|
|
|
setRecurPref: function TPE_setRecurPrefs(value) {
|
|
// value is NEVER_SUBMIT, ALWAYS_SUBMIT, or ASK_EACH_TIME
|
|
let prefName = RECUR_PREF_PREFIX + this._id;
|
|
this._logger.info("Setting recur pref to " + value);
|
|
Application.prefs.setValue(prefName, value);
|
|
}
|
|
};
|
|
TestPilotExperiment.prototype.__proto__ = TestPilotTask;
|
|
|
|
function TestPilotBuiltinSurvey(surveyInfo) {
|
|
this._init(surveyInfo);
|
|
}
|
|
TestPilotBuiltinSurvey.prototype = {
|
|
_init: function TestPilotBuiltinSurvey__init(surveyInfo) {
|
|
this._taskInit(surveyInfo.surveyId,
|
|
surveyInfo.surveyName,
|
|
surveyInfo.surveyUrl,
|
|
surveyInfo.summary,
|
|
surveyInfo.thumbnail);
|
|
this._studyId = surveyInfo.uploadWithExperiment; // what study do we belong to
|
|
this._versionNumber = surveyInfo.versionNumber;
|
|
this._questions = surveyInfo.surveyQuestions;
|
|
this._explanation = surveyInfo.surveyExplanation;
|
|
},
|
|
|
|
get taskType() {
|
|
return TaskConstants.TYPE_SURVEY;
|
|
},
|
|
|
|
get surveyExplanation() {
|
|
return this._explanation;
|
|
},
|
|
|
|
get surveyQuestions() {
|
|
return this._questions;
|
|
},
|
|
|
|
get currentStatusUrl() {
|
|
let param = "?eid=" + this._id;
|
|
return "chrome://testpilot/content/take-survey.html" + param;
|
|
},
|
|
|
|
get defaultUrl() {
|
|
return this.currentStatusUrl;
|
|
},
|
|
|
|
get relatedStudyId() {
|
|
return this._studyId;
|
|
},
|
|
|
|
onDetailPageOpened: function TPS_onDetailPageOpened() {
|
|
if (this._status < TaskConstants.STATUS_IN_PROGRESS) {
|
|
this.changeStatus( TaskConstants.STATUS_IN_PROGRESS, true );
|
|
}
|
|
},
|
|
|
|
get oldAnswers() {
|
|
let surveyResults =
|
|
Application.prefs.getValue(SURVEY_ANSWER_PREFIX + this._id, null);
|
|
if (!surveyResults) {
|
|
return null;
|
|
} else {
|
|
this._logger.info("Trying to json.parse this: " + surveyResults);
|
|
let surveyJson = sanitizeJSONStrings( JSON.parse(surveyResults) );
|
|
return surveyJson["answers"];
|
|
}
|
|
},
|
|
|
|
store: function TestPilotSurvey_store(surveyResults, callback) {
|
|
/* Store answers in preferences store; then, upload the survey data
|
|
* if this is a survey with an associated study (the matching GUIDs
|
|
* will be used to associate the two datasets server-side).
|
|
* If it's not one with an associated survey, just store and set status
|
|
* to submitted. Either way, call the callback with true/false for
|
|
* success/failure.*/
|
|
surveyResults = sanitizeJSONStrings(surveyResults);
|
|
let prefName = SURVEY_ANSWER_PREFIX + this._id;
|
|
// Also store survey version number
|
|
if (this._versionNumber) {
|
|
surveyResults["version_number"] = this._versionNumber;
|
|
}
|
|
Application.prefs.setValue(prefName, JSON.stringify(surveyResults));
|
|
if (this._studyId) {
|
|
this._upload(callback, 0);
|
|
} else {
|
|
this.changeStatus(TaskConstants.STATUS_SUBMITTED);
|
|
callback(true);
|
|
}
|
|
},
|
|
|
|
_prependMetadataToJSON: function TestPilotSurvey__prependToJson(callback) {
|
|
let json = {};
|
|
let self = this;
|
|
MetadataCollector.getMetadata(function(md) {
|
|
json.metadata = md;
|
|
// Include guid of the study that this survey is related to, so we
|
|
// can match them up server-side.
|
|
let guid = Application.prefs.getValue(GUID_PREF_PREFIX + self._studyId, "");
|
|
json.metadata.task_guid = guid;
|
|
let pref = SURVEY_ANSWER_PREFIX + self._id;
|
|
let surveyAnswers = JSON.parse(Application.prefs.getValue(pref, "{}"));
|
|
json.survey_data = sanitizeJSONStrings(surveyAnswers);
|
|
callback(JSON.stringify(json));
|
|
});
|
|
},
|
|
|
|
// Upload function for survey -- TODO this duplicates a lot of code
|
|
// from study._upload().
|
|
_upload: function TestPilotSurvey__upload(callback, retryCount) {
|
|
let self = this;
|
|
this._prependMetadataToJSON(function(params) {
|
|
let req =
|
|
Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
|
|
createInstance(Ci.nsIXMLHttpRequest);
|
|
let url = self.uploadUrl;
|
|
|
|
req.open("POST", url, true);
|
|
req.setRequestHeader("Content-type", "application/json");
|
|
req.setRequestHeader("Content-length", params.length);
|
|
req.setRequestHeader("Connection", "close");
|
|
req.onreadystatechange = function(aEvt) {
|
|
if (req.readyState == 4) {
|
|
if (req.status == 200) {
|
|
self._logger.info(
|
|
"DATA WAS POSTED SUCCESSFULLY " + req.responseText);
|
|
if (self._uploadRetryTimer) {
|
|
self._uploadRetryTimer.cancel(); // Stop retrying - it worked!
|
|
}
|
|
self.changeStatus(TaskConstants.STATUS_SUBMITTED);
|
|
callback(true);
|
|
} else {
|
|
self._logger.warn("ERROR POSTING DATA: " + req.responseText);
|
|
self._uploadRetryTimer =
|
|
Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
|
|
if (!retryCount) {
|
|
retryCount = 0;
|
|
}
|
|
let interval =
|
|
Application.prefs.getValue(RETRY_INTERVAL_PREF, 3600000); // 1 hour
|
|
let delay =
|
|
parseInt(Math.random() * Math.pow(2, retryCount) * interval);
|
|
self._uploadRetryTimer.initWithCallback(
|
|
{ notify: function(timer) {
|
|
self._upload(callback, retryCount++);
|
|
} }, (interval + delay), Ci.nsITimer.TYPE_ONE_SHOT);
|
|
callback(false);
|
|
}
|
|
}
|
|
};
|
|
req.send(params);
|
|
});
|
|
}
|
|
};
|
|
TestPilotBuiltinSurvey.prototype.__proto__ = TestPilotTask;
|
|
|
|
function TestPilotWebSurvey(surveyInfo) {
|
|
this._init(surveyInfo);
|
|
}
|
|
TestPilotWebSurvey.prototype = {
|
|
_init: function TestPilotWebSurvey__init(surveyInfo) {
|
|
this._taskInit(surveyInfo.surveyId,
|
|
surveyInfo.surveyName,
|
|
surveyInfo.surveyUrl,
|
|
surveyInfo.summary,
|
|
surveyInfo.thumbnail);
|
|
this._logger.info("Initing survey. This._status is " + this._status);
|
|
},
|
|
|
|
get taskType() {
|
|
return TaskConstants.TYPE_SURVEY;
|
|
},
|
|
|
|
get defaultUrl() {
|
|
return this.infoPageUrl;
|
|
},
|
|
|
|
get showMoreInfoLink() {
|
|
return false;
|
|
},
|
|
|
|
onDetailPageOpened: function TPWS_onDetailPageOpened() {
|
|
/* Once you view the URL of the survey, we'll assume you've taken it.
|
|
* There's no reliable way to tell whether you have or not, so let's
|
|
* default to not bugging the user about it again.
|
|
*/
|
|
if (this._status < TaskConstants.STATUS_SUBMITTED) {
|
|
this.changeStatus( TaskConstants.STATUS_SUBMITTED, true );
|
|
}
|
|
}
|
|
};
|
|
TestPilotWebSurvey.prototype.__proto__ = TestPilotTask;
|
|
|
|
|
|
function TestPilotStudyResults(resultsInfo) {
|
|
this._init(resultsInfo);
|
|
};
|
|
TestPilotStudyResults.prototype = {
|
|
_init: function TestPilotStudyResults__init(resultsInfo) {
|
|
this._taskInit( resultsInfo.id,
|
|
resultsInfo.title,
|
|
resultsInfo.url,
|
|
resultsInfo.summary,
|
|
resultsInfo.thumbnail);
|
|
this._studyId = resultsInfo.studyId; // what study do we belong to
|
|
this._pubDate = Date.parse(resultsInfo.date);
|
|
},
|
|
|
|
get taskType() {
|
|
return TaskConstants.TYPE_RESULTS;
|
|
},
|
|
|
|
get publishDate() {
|
|
return this._pubDate;
|
|
},
|
|
|
|
get relatedStudyId() {
|
|
return this._studyId;
|
|
}
|
|
};
|
|
TestPilotStudyResults.prototype.__proto__ = TestPilotTask;
|
|
|
|
function TestPilotLegacyStudy(studyInfo) {
|
|
this._init(studyInfo);
|
|
};
|
|
TestPilotLegacyStudy.prototype = {
|
|
_init: function TestPilotLegacyStudy__init(studyInfo) {
|
|
let stat = Application.prefs.getValue(STATUS_PREF_PREFIX + studyInfo.id,
|
|
null);
|
|
this._taskInit( studyInfo.id,
|
|
studyInfo.name,
|
|
studyInfo.url,
|
|
studyInfo.summary,
|
|
studyInfo.thumbnail );
|
|
|
|
/* Only three statuses are valid for legacy studies: It must be either
|
|
* canceled, archived (meaning you submitted it), or missed (you never ran it).
|
|
* Set status to one of these.
|
|
*/
|
|
switch (stat) {
|
|
case TaskConstants.STATUS_CANCELLED:
|
|
case TaskConstants.STATUS_ARCHIVED:
|
|
case TaskConstants.STATUS_MISSED:
|
|
// Keep that status, so do nothing
|
|
break;
|
|
case TaskConstants.STATUS_SUBMITTED:
|
|
// Change submitted to archived
|
|
this.changeStatus(TaskConstants.STATUS_ARCHIVED, true);
|
|
break;
|
|
default:
|
|
// Anything else means you missed it
|
|
this.changeStatus(TaskConstants.STATUS_MISSED, true);
|
|
}
|
|
|
|
if (studyInfo.duration) {
|
|
let prefName = START_DATE_PREF_PREFIX + this._id;
|
|
let startDateString = Application.prefs.getValue(prefName, null);
|
|
if (startDateString) {
|
|
this._startDate = Date.parse(startDateString);
|
|
this._endDate = this._startDate + duration * (24 * 60 * 60 * 1000);
|
|
}
|
|
}
|
|
},
|
|
|
|
get taskType() {
|
|
return TaskConstants.TYPE_LEGACY;
|
|
}
|
|
// TODO test that they don't say "thanks for contributing" if the
|
|
// user didn't actually complete them...
|
|
};
|
|
TestPilotLegacyStudy.prototype.__proto__ = TestPilotTask; |