/* ***** 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 * Jono X * Raymond Lee * Jorge Villalobos * * 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, // TODO make this a lazy initializer too? 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); } }, 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; } // Don't show first run page in ffx4 beta version. } // 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, onCloseCallback) { /* TODO: Refactor the arguments of this function, it's getting really * unweildly.... maybe pass in an object, or even make a notification an * object that you create and then call .show() on. */ // If there are multiple windows, show notifications in the frontmost // window. let window = this._getFrontBrowserWindow(); let doc = window.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(window, onCloseCallback); }; } else { submitBtn.setAttribute("label", this._stringBundle.GetStringFromName("testpilot.submit")); // Functionality for submit button: submitBtn.onclick = function() { self._hideNotification(window, onCloseCallback); 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(window, onCloseCallback); } }; link.setAttribute("hidden", false); } else { link.setAttribute("hidden", true); } closeBtn.onclick = function() { self._hideNotification(window, onCloseCallback); }; // 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(window, onCloseCallback) { /* Note - we take window as an argument instead of just using the frontmost * window because the window order might have changed since the notification * appeared and we want to be sure we close the notification in the same * window as we opened it in! */ let popup = window.document.getElementById("pilot-notification-popup"); popup.hidden = true; popup.setAttribute("open", "false"); popup.removeAttribute("tpisextensionupdate"); popup.hidePopup(); if (onCloseCallback) { onCloseCallback(); } }, _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_PENDING || task.status == TaskConstants.STATUS_NEW) { if (task.taskType == TaskConstants.TYPE_EXPERIMENT) { this._showNotification( task, false, 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, false, function() { /* on close callback (Bug 575767) -- when the "new study * starting" popup is dismissed, then the study can start. */ task.changeStatus(TaskConstants.STATUS_IN_PROGRESS, true); TestPilotSetup.reloadRemoteExperiments(); }); return; } else if (task.taskType == TaskConstants.TYPE_SURVEY) { this._showNotification( task, false, 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; } };