Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,19 @@
'use strict';
/* jshint node: true */
var utils = require('utils');
exports.execute = function(config) {
utils.copyToStage(config);
var init = utils.getFile(config.STAGE_APP_DIR, 'js', 'config.js');
var content = {
'api_url': 'https://find.firefox.com',
'api_version': '1',
};
var distDir = config.GAIA_DISTRIBUTION_DIR;
utils.writeContent(init, 'Config = ' +
utils.getDistributionFileContent('findmydevice', content, distDir) +
';');
};

View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<!-- Shared code -->
<script src="shared/js/dump.js"></script>
<script src="shared/js/passcode_helper.js"></script>
<script defer src="shared/js/settings_listener.js"></script>
<script defer src="shared/js/settings_helper.js"></script>
<script defer src="shared/js/async_storage.js"></script>
<script defer src="shared/js/settings_url.js"></script>
<script defer src="shared/js/findmydevice_iac_api.js"></script>
<script defer src="js/lib/hawk.js"></script>
<script defer src="js/config.js"></script>
<script defer src="js/requester.js"></script>
<script defer src="js/commands.js"></script>
<script defer src="js/findmydevice.js"></script>
</head>
<body></body>
</html>

View file

@ -0,0 +1,220 @@
/* global DUMP, SettingsHelper, SettingsListener, SettingsURL, FindMyDevice,
PasscodeHelper */
'use strict';
var Commands = {
TRACK_UPDATE_INTERVAL_MS: 10000,
_ringer: null,
_lockscreenEnabled: false,
_lockscreenPassCodeEnabled: false,
_geolocationEnabled: false,
init: function fmdc_init() {
var ringer = this._ringer = new Audio();
ringer.mozAudioChannelType = 'ringer';
ringer.loop = true;
var ringtoneURL = new SettingsURL();
SettingsListener.observe('dialer.ringtone', '', function(value) {
var ringing = !ringer.paused;
ringer.pause();
ringer.src = ringtoneURL.set(value);
if (ringing) {
ringer.play();
}
});
var self = this;
SettingsListener.observe('lockscreen.enabled', false, function(value) {
self._lockscreenEnabled = value;
});
SettingsListener.observe('lockscreen.passcode-lock.enabled', false,
function(value) {
self._lockscreenPassCodeEnabled = value;
}
);
SettingsListener.observe('geolocation.enabled', false, function(value) {
self._geolocationEnabled = value;
});
},
getEnabledCommands: function fmdc_get_enabled_commands() {
var commands = Object.keys(this._commands);
if (!this._geolocationEnabled) {
var idx = commands.indexOf('track');
if (idx >= 0) {
commands.splice(idx, 1);
}
}
return commands;
},
invokeCommand: function fmdc_get_command(name, args) {
FindMyDevice.beginHighPriority('command');
this._commands[name].apply(this, args);
},
deviceHasPasscode: function fmdc_device_has_passcode() {
return !!(this._lockscreenEnabled && this._lockscreenPassCodeEnabled);
},
_watchPositionId: null,
_trackTimeoutId: null,
_ringTimeoutId: null,
_commands: {
track: function fmdc_track(duration, reply) {
var self = this;
function stop() {
navigator.geolocation.clearWatch(self._watchPositionId);
self._watchPositionId = null;
clearTimeout(self._trackTimeoutId);
self._trackTimeoutId = null;
SettingsHelper('findmydevice.tracking').set(false);
FindMyDevice.endHighPriority('command');
}
if (this._watchPositionId !== null || this._trackTimeoutId !== null) {
// already tracking
stop();
}
if (duration === 0) {
if (reply) {
reply(true);
}
FindMyDevice.endHighPriority('command');
return;
}
var lastPositionTimestamp = 0;
// start watching the current position, but throttle updates to one
// every TRACK_UPDATE_INTERVAL_MS
SettingsHelper('findmydevice.tracking').set(true);
self._watchPositionId = navigator.geolocation.watchPosition(
function(position) {
DUMP('received location (' +
position.coords.latitude + ', ' +
position.coords.longitude + ')'
);
var timeElapsed = position.timestamp - lastPositionTimestamp;
if (timeElapsed < self.TRACK_UPDATE_INTERVAL_MS) {
DUMP('ignoring position due to throttling');
return;
}
lastPositionTimestamp = position.timestamp;
reply(true, position);
}, function(error) {
reply(false, 'failed to get location: ' + error.message);
});
duration = (isNaN(duration) || duration < 0) ? 1 : duration;
self._trackTimeoutId = setTimeout(stop, duration * 1000);
},
erase: function fmdc_erase(reply) {
navigator.mozPower.factoryReset('wipe');
// factoryReset() won't return, unless we're testing,
// in which case mozPower is a mock. The reply() below
// is thus only used for testing.
reply(true);
},
killswitch: function fmdc_killswitch(reply) {
if (navigator.mozKillSwitch) {
navigator.mozKillSwitch.enable();
}
FindMyDevice.endHighPriority('command');
reply(true);
},
lock: function fmdc_lock(message, passcode, reply) {
var pr;
var settings = {
'lockscreen.enabled': true,
'lockscreen.notifications-preview.enabled': false,
'lockscreen.passcode-lock.enabled': true,
'lockscreen.lock-immediately': true
};
if (message) {
settings['lockscreen.lock-message'] = message;
}
if (passcode) {
pr = PasscodeHelper.set(passcode);
}
var request = SettingsListener.getSettingsLock().set(settings);
Promise.all([pr, request]).then(() => {
reply(true);
}).catch(() => {
reply(false);
});
FindMyDevice.endHighPriority('command');
},
ring: function fmdc_ring(duration, reply) {
var ringer = this._ringer;
var stop = function() {
ringer.pause();
ringer.currentTime = 0;
clearTimeout(this._ringTimeoutId);
this._ringTimeoutId = null;
FindMyDevice.endHighPriority('command');
}.bind(this);
var ringing = !ringer.paused || this._ringTimeoutId !== null;
if (ringing || duration === 0) {
if (ringing && duration === 0) {
stop();
}
if (reply) {
reply(true);
}
FindMyDevice.endHighPriority('command');
return;
}
var request = SettingsListener.getSettingsLock().set({
// hard-coded max volume taken from
// https://wiki.mozilla.org/WebAPI/AudioChannels
'audio.volume.notification': 15
});
request.onsuccess = function() {
ringer.play();
reply(true);
};
request.onerror = function() {
reply(false, 'failed to set volume');
};
this._ringTimeoutId = setTimeout(stop, duration * 1000);
}
}
};
Commands.init();

View file

@ -0,0 +1,665 @@
/* global SettingsHelper */
/* global asyncStorage */
/* global Requester */
/* global Commands */
/* global Config */
/* global DUMP */
/* global IAC_API_WAKEUP_REASON_ENABLED_CHANGED */
/* global IAC_API_WAKEUP_REASON_LOGIN */
/* global IAC_API_WAKEUP_REASON_LOGOUT */
/* global IAC_API_WAKEUP_REASON_STALE_REGISTRATION */
/* global IAC_API_WAKEUP_REASON_TRY_DISABLE */
/* global IAC_API_WAKEUP_REASON_LOCKSCREEN_CLOSED */
'use strict';
var FindMyDevice = {
_state: null,
_registering: false,
_refreshingClientID: false,
_reply: {},
_registered: false,
_registeredHelper: null,
_enabled: false,
_enabledHelper: null,
_loggedIn: false,
_currentClientID: '',
_currentClientIDHelper: null,
_canDisable: false,
_canDisableHelper: null,
_retryCount: -1,
_retryCountHelper: null,
_disableAttempt: false,
// There are two situations in which we want to make sure
// FMD is alive:
// - when it is running client logic that accesses the network;
// - when it is executing a command with a duration.
// Command wakelocks are managed by individual commands.
// For client logic wakelocks, we use the fact that when a network
// request succeeds, we'll either update 'findmydevice.registered',
// or 'findmydevice.can-disable', or process a command and set
// a 'ping' timer, and when it fails, we'll set a timer to retry.
// Since each network transaction finishes with either a change
// in one of these settings or with a timer, we release the locks
// in the observers for the settings and in _scheduleAlarm.
_highPriorityWakeLocks: {
clientLogic: [],
command: []
},
_fxaReady: false,
init: function fmd_init() {
var self = this;
navigator.mozId.watch({
wantIssuer: 'firefox-accounts',
audience: Config.api_url,
onready: self._onReady.bind(self),
onlogin: self._onLogin.bind(self),
onlogout: self._onLogout.bind(self),
onerror: self._onFxAError.bind(self)
});
},
_observeSettings: function fmd_observe_settings() {
var settings = navigator.mozSettings;
settings.addObserver('findmydevice.registered',
this._onRegisteredChanged.bind(this));
settings.addObserver('findmydevice.enabled',
this._onEnabledChanged.bind(this));
settings.addObserver('findmydevice.current-clientid',
this._onClientIDChanged.bind(this));
// We only allow disabling Find My Device if the same person
// who first enabled it is logged in to FxA, and in that case
// 'findmydevice.can-disable' is true.
// However, note that FMD doesn't store the user's email address,
// but rather a client ID that is derived from the FxA assertion
// and stored in this._state.clientid.
settings.addObserver('findmydevice.can-disable',
this._onCanDisableChanged.bind(this));
},
_onReady: function fmd_fxa_onready() {
var self = this;
this._loadState(function() {
self._initSettings(self._initMessageHandlers.bind(self));
});
this._fxaReady = true;
this._observeSettings();
},
_loadState: function fmd_load_state(callback) {
var self = this;
asyncStorage.getItem('findmydevice-state', function(state) {
self._state = state;
if (state) {
Requester.setHawkCredentials(state.deviceid, state.secret);
}
callback && callback();
});
},
_initSettings: function fmd_init_settings(callback) {
// for each setting we're interested in, create a SettingsHelper,
// cache its initial value in a property (e.g., _enabled), and
// store the SettingsHelper as another property (e.g., _enabledHelper).
var settingsToProperties = {
// setting name : property name for cached value
'findmydevice.enabled': '_enabled',
'findmydevice.registered': '_registered',
'findmydevice.current-clientid': '_currentClientID',
'findmydevice.can-disable': '_canDisable',
'findmydevice.retry-count': '_retryCount'
};
var settings = Object.keys(settingsToProperties);
var loaded = 0;
var totalSettings = Object.keys(settingsToProperties).length;
settings.forEach(function(s) {
var prop = settingsToProperties[s];
var helper = this[prop + 'Helper'] = SettingsHelper(s);
helper.get((function(value) {
this[prop] = value;
if (++loaded === totalSettings) {
callback && callback();
}
}).bind(this));
}, this);
},
_initMessageHandlers: function fmd_init_message_handlers() {
navigator.mozSetMessageHandler('push', (function(message) {
DUMP('findmydevice got push notification!');
this._contactServer();
}).bind(this));
navigator.mozSetMessageHandler('push-register', (function(message) {
DUMP('findmydevice lost push endpoint, re-registering');
this.beginHighPriority('clientLogic');
this._registeredHelper.set(false);
}).bind(this));
navigator.mozSetMessageHandler('alarm', (function(alarm) {
DUMP('findmydevice alarm!');
this._contactServer();
this._refreshClientIDIfRegistered(false);
}).bind(this));
navigator.mozSetMessageHandler('connection', (function(request) {
var port = request.port;
port.onmessage = (function(event) {
if (request.keyword === 'findmydevice-wakeup') {
DUMP('got wake up request');
var reason = event.data;
if (reason === IAC_API_WAKEUP_REASON_ENABLED_CHANGED) {
DUMP('enabled state changed, trying to reach the server');
// Ensure the retry counter is reset to 0 on enable
this._retryCountHelper.set(0);
this._contactServer();
} else if (reason === IAC_API_WAKEUP_REASON_STALE_REGISTRATION) {
DUMP('stale registration, re-registering');
this.beginHighPriority('clientLogic');
this._registeredHelper.set(false);
} else if (reason === IAC_API_WAKEUP_REASON_LOGIN) {
DUMP('new login, invalidating client id');
this.beginHighPriority('clientLogic');
this._loggedIn = true;
this._currentClientIDHelper.set('');
} else if (reason === IAC_API_WAKEUP_REASON_LOGOUT) {
DUMP('logout, invalidating client id');
this.beginHighPriority('clientLogic');
this._loggedIn = false;
this._currentClientIDHelper.set('');
} else if (reason === IAC_API_WAKEUP_REASON_TRY_DISABLE) {
DUMP('refreshing client id and attempting to disable');
this._disableAttempt = true;
this._refreshClientIDIfRegistered(true);
} else if (reason === IAC_API_WAKEUP_REASON_LOCKSCREEN_CLOSED) {
DUMP('unlocked');
this._onLockscreenClosed();
}
return;
}
if (request.keyword === 'findmydevice-test') {
DUMP('got request for test command!');
this._processCommands(event.data, true);
}
}).bind(this);
}).bind(this));
},
_contactServer: function fmd_contact_server() {
this.beginHighPriority('clientLogic');
if (this._registered && this._enabled) {
this._replyAndFetchCommands();
} else if (this._registered && !this._enabled) {
this._reportDisabled();
} else if (!this._registered && this._enabled) {
this._register();
} else {
// XXX(ggp) this should never happen, but let's play
// it safe and release lock we acquired
DUMP('can\'t contact the server while not registered and not enabled!!');
this.endHighPriority('clientLogic');
}
},
_reportDisabled: function fmd_report_disabled() {
DUMP('reporting disabled');
Requester.post(
this._getCommandEndpoint(),
{enabled: false},
this._handleServerResponse.bind(this),
this._handleServerError.bind(this));
},
_register: function fmd_register() {
DUMP('findmydevice attempting registration.');
DUMP('registering: ' + this._registering);
if (this._registering) {
this._registering = false;
this.endHighPriority('clientLogic');
return;
}
this._registering = true;
if (this._state === null) {
// We are not registered yet, so we need to give the server a FxA
// assertion. Request a fresh one here and continue the registration
// process from the login callback.
if (!this._loggedIn) {
// Can't request() while not logged in, as that would bring up the
// FxA dialog. Attempting a first registration while logged out is
// theoretically possible (if, for example, a first registration
// failed, and this is an alarm-driven re-attempt, and the user logged
// out while we slept before this attempt), but it's a very unlikely
// corner case. For now, just give up if this happens, but we still
// need to notify the user somehow (bug 1013423).
this._registering = false;
this.endHighPriority('clientLogic');
return;
}
navigator.mozId.request();
} else {
// We don't need to send assertions on re-registrations, just rely on
// our client ID and HAWK signature to authenticate us. This works even
// if we're logged out of FxA, so use null for the assertion.
this._continueRegistration(null);
}
},
_onLogin: function fmd_on_login(assertion) {
DUMP('logged in to FxA');
// XXX(ggp) When initializing, we'll process FxA's automatic invocation
// of onlogin/onlogout before any IAC message, so use these calls to
// initialize _loggedIn. However, when FMD is already running, there's
// no guaranteee that onlogin/onlogout will fire before the IAC handler,
// so we have dedicated IAC messages (IAC_API_WAKEUP_REASON_LOGIN and
// IAC_API_WAKEUP_REASON_LOGOUT) to cover login state changes, and we
// use the handlers for these messages to update _loggedIn.
this._loggedIn = true;
if (this._refreshingClientID) {
DUMP('resuming client id refresh');
this._fetchClientID(assertion);
}
if (!this._enabled || !this._registering) {
return;
}
// We are in the middle of a registration, so continue here
this._continueRegistration(assertion);
},
_continueRegistration: function fmd_continue_registration(assertion) {
var self = this;
if (assertion == null && this._state == null) {
// this shouldn't happen
throw new Error('Trying to register with no assertion and no state!');
}
var pushRequest = navigator.push.register();
pushRequest.onsuccess = function fmd_push_handler() {
DUMP('findmydevice received push endpoint!');
var endpoint = pushRequest.result;
if (self._enabled) {
self._requestRegistration(assertion, endpoint);
}
self._registering = false;
};
pushRequest.onerror = function fmd_push_error_handler() {
DUMP('findmydevice push request failed!');
self._registering = false;
self._countRegistrationRetry();
};
},
_requestRegistration: function fmd_request_registration(assertion, endpoint) {
var obj = {
pushurl: endpoint,
accepts: Commands.getEnabledCommands()
};
if (assertion != null) {
obj.assert = assertion;
}
if (this._state !== null) {
obj.deviceid = this._state.deviceid;
}
var self = this;
Requester.post('/register/', obj, function(response) {
DUMP('findmydevice successfully registered: ', response);
asyncStorage.setItem('findmydevice-state', response, function() {
self._registeredHelper.set(true);
});
}, this._handleServerError.bind(this));
},
_onLogout: function fmd_fxa_onlogout() {
DUMP('logged out of FxA');
this._loggedIn = false;
},
_onFxAError: function fmd_on_error(error) {
DUMP('FxA error: ' + error);
if (!this._fxaReady) {
// FIXME(ggp) workaround for bug 1040935, if FxA errors out
// while we are initializing (usually due to an unverified
// account), just give up for now.
window.close();
}
if (this._refreshingClientID) {
this._cancelClientIDRefresh();
}
this._scheduleAlarm('retry');
},
_cancelClientIDRefresh: function fmd_cancel_clientid_refresh() {
this._disableAttempt = false;
this._refreshingClientID = false;
// If we've been woken up by the Settings app because of a disable
// attempt, it expects us to change these two settings at the end
// of the operation, so set them to their current values if we're
// canceling to make sure the state in the Settings is updated
// properly.
this._enabledHelper.set(this._enabled);
this._currentClientIDHelper.set(this._currentClientID);
// XXX(ggp) no need to unlock the 'currentClient' lock here, let
// _onClientIDChanged do it for us
},
_scheduleAlarm: function fmd_schedule_alarm(mode) {
var nextAlarm = new Date();
if (mode === 'ping') {
// this is just a regular ping to the server to make
// sure we're still registered, so use a long interval
nextAlarm.setHours(nextAlarm.getHours() + 6);
} else if (mode === 'retry') {
// something went wrong when registering or talking to the
// server, we should check back shortly
var interval = 1 + Math.floor(5 * Math.random());
nextAlarm.setMinutes(nextAlarm.getMinutes() + interval);
} else {
DUMP('invalid alarm mode!');
return;
}
var self = this;
var request = navigator.mozAlarms.getAll();
request.onsuccess = function fmd_alarms_get_all() {
this.result.forEach(function(alarm) {
navigator.mozAlarms.remove(alarm.id);
});
var data = {type: 'findmydevice-alarm'};
var request = navigator.mozAlarms.add(nextAlarm, 'honorTimezone', data);
request.onsuccess = function() {
self.endHighPriority('clientLogic');
};
};
},
_replyAndFetchCommands: function fmd_reply_and_fetch() {
this._reply.has_passcode = Commands.deviceHasPasscode();
Requester.post(
this._getCommandEndpoint(),
this._reply,
this._handleServerResponse.bind(this),
this._handleServerError.bind(this));
this._reply = {};
},
_onRegisteredChanged: function fmd_registered_changed(event) {
this._registered = event.settingValue;
DUMP('registered: ' + this._registered);
if (!this._registered) {
this._contactServer();
this.endHighPriority('clientLogic');
} else {
this._loadState((function() {
this._contactServer();
this._currentClientIDHelper.set('');
// XXX(ggp) we're still holding a 'clientLogic' lock, but let
// _onClientIDChanged or _onCanDisableChanged release it (depending
// on whether we're logged in or not).
}).bind(this));
}
},
_onEnabledChanged: function fmd_enabled_changed(event) {
// No need to contact the server here if we've been enabled,
// since that means we'll get a wake up request from the System
// app
this._enabled = event.settingValue;
DUMP('enabled: ' + this._enabled);
},
_onClientIDChanged: function fmd_client_id_changed(event) {
this._currentClientID = event.settingValue;
DUMP('current id set to: ', this._currentClientID);
if (this._loggedIn && this._currentClientID === '') {
this._refreshClientIDIfRegistered(false);
this.endHighPriority('clientLogic');
} else if (this._registered) {
this._canDisableHelper.set(
this._loggedIn &&
this._currentClientID === this._state.clientid);
} else {
this.endHighPriority('clientLogic');
}
},
_onCanDisableChanged: function fmd_can_disable_changed(event) {
if (event.settingValue === true && this._disableAttempt) {
this._enabledHelper.set(false);
}
this._disableAttempt = false;
this.endHighPriority('clientLogic');
},
_refreshClientIDIfRegistered: function fmd_refresh_client_id(forceReauth) {
DUMP('refreshing client id if registered and logged in: ',
{registered: this._registered, loggedIn: this._loggedIn});
if (!this._registered || !this._loggedIn) {
return;
}
DUMP('requesting assertion to refresh client id, forceReauth: ' +
forceReauth);
this.beginHighPriority('clientLogic');
this._refreshingClientID = true;
var mozIdRequestOptions = {};
if (forceReauth) {
mozIdRequestOptions.refreshAuthentication = 0;
mozIdRequestOptions.oncancel = this._cancelClientIDRefresh.bind(this);
}
navigator.mozId.request(mozIdRequestOptions);
},
_onLockscreenClosed: function fmd_on_lockscreen_closed() {
if (Commands.deviceHasPasscode()) {
DUMP('cancelling ring and track');
Commands.invokeCommand('ring', [0]);
Commands.invokeCommand('track', [0]);
}
},
_fetchClientID: function fmd_fetch_client_id(assertion) {
Requester.post('/validate/', {assert: assertion},
this._onClientIDResponse.bind(this),
this._onClientIDServerError.bind(this));
this._refreshingClientID = false;
},
_onClientIDResponse: function fmd_on_client_id(response) {
DUMP('got clientid reponse: ', response);
if (response.valid) {
this._currentClientIDHelper.set(response.uid);
return;
}
DUMP('failed to verify assertion for client id!');
this._scheduleAlarm('retry');
},
_onClientIDServerError: function fmd_on_client_id_error(err) {
DUMP('failed to fetch client id with status: ' + err.status);
this._scheduleAlarm('retry');
this._disableAttempt = false;
},
_handleServerResponse: function(response, testing) {
if (response && (this._enabled || testing)) {
this._processCommands(response);
}
this._scheduleAlarm('ping');
},
_processCommands: function fmd_process_commands(cmdobj) {
function noop() {} // callback for testing
for (var cmd in cmdobj) {
// map server (short) commands to methods in the
// commands object, and parse the arguments.
var argsObj = cmdobj[cmd], command, args;
switch (cmd) {
case 'e':
command = 'erase';
args = [];
break;
case 'k':
command = 'killswitch';
args = [];
break;
case 'l':
command = 'lock';
args = [argsObj.m, argsObj.c];
break;
case 'r':
command = 'ring';
args = [parseInt(argsObj.d, 10)];
break;
case 't':
command = 'track';
args = [parseInt(argsObj.d, 10)];
break;
default:
this._replyCallback(cmd, false, 'command not available');
return;
}
DUMP('command ' + cmd + ', args ' + JSON.stringify(args));
// add the callback as the last argument
if (cmdobj.testing !== true) {
args.push(this._replyCallback.bind(this, cmd));
} else {
args.push(noop);
}
Commands.invokeCommand(command, args);
}
},
_countRegistrationRetry: function fmd_count_registration_retry (){
this._scheduleAlarm('retry');
if (!navigator.onLine) {
return;
}
if (!this._registered) {
this._retryCountHelper.get(count => {
this._retryCountHelper.set((count || 0) + 1);
});
}
},
_handleServerError: function fmd_handle_server_error(err) {
DUMP('findmydevice request failed with status: ' + err.status);
if (err.status === 401 && this._registered) {
this._registeredHelper.set(false);
} else {
this._countRegistrationRetry();
}
},
_replyCallback: function fmd_reply(cmd, ok, retval) {
var value = {ok: ok};
if (cmd === 't' && ok === true && retval !== undefined) {
value.la = retval.coords.latitude;
value.lo = retval.coords.longitude;
value.acc = retval.coords.accuracy;
value.ti = retval.timestamp;
} else if (ok === false) {
value.error = retval;
}
this._reply[cmd] = value;
this._contactServer();
},
_getCommandEndpoint: function fmd_get_command_endpoint() {
return '/cmd/' + this._state.deviceid;
},
beginHighPriority: function(reason) {
DUMP('begin high priority section, reason: ', reason);
if (Object.keys(this._highPriorityWakeLocks).indexOf(reason) === -1) {
DUMP('unknown reason for high priority section?!');
return;
}
DUMP('acquiring one wakelock, wakelocks are: ',
this._highPriorityWakeLocks);
this._highPriorityWakeLocks[reason].push(
navigator.requestWakeLock('high-priority'));
},
endHighPriority: function(reason) {
DUMP('end high priority section, reason: ', reason);
if (!this._highPriorityWakeLocks[reason]) {
return;
}
DUMP('releasing one wakelock, wakelocks are: ',
this._highPriorityWakeLocks);
this._highPriorityWakeLocks[reason].pop().unlock();
}
};
FindMyDevice.init();

View file

@ -0,0 +1,507 @@
/*
HTTP Hawk Authentication Scheme
Copyright (c) 2012-2013, Eran Hammer <eran@hueniverse.com>
MIT Licensed
*/
// Declare namespace
var hawk = {};
// Export if used as a module
if (typeof module !== "undefined" && module.exports) {
module.exports = hawk;
}
hawk.client = {
// Generate an Authorization header for a given request
/*
uri: 'http://example.com/resource?a=b'
method: HTTP verb (e.g. 'GET', 'POST')
options: {
// Required
credentials: {
id: 'dh37fgj492je',
key: 'aoijedoaijsdlaksjdl',
algorithm: 'sha256' // 'sha1', 'sha256'
},
// Optional
ext: 'application-specific', // Application specific data sent via the ext attribute
timestamp: Date.now() / 1000, // A pre-calculated timestamp in seconds
nonce: '2334f34f', // A pre-generated nonce
localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided)
payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided)
contentType: 'application/json', // Payload content-type (ignored if hash provided)
hash: 'U4MKKSmiVxk37JCCrAVIjV=', // Pre-calculated payload hash
app: '24s23423f34dx', // Oz application id
dlg: '234sz34tww3sd' // Oz delegated-by application id
}
*/
header: function (uri, method, options) {
var result = {
field: '',
artifacts: {}
};
// Validate inputs
if (!uri || (typeof uri !== 'string' && typeof uri !== 'object') ||
!method || typeof method !== 'string' ||
!options || typeof options !== 'object') {
result.err = 'Invalid argument type';
return result;
}
// Application time
var timestamp = options.timestamp || Math.floor((hawk.utils.now() + (options.localtimeOffsetMsec || 0)) / 1000)
// Validate credentials
var credentials = options.credentials;
if (!credentials ||
!credentials.id ||
!credentials.key ||
!credentials.algorithm) {
result.err = 'Invalid credential object';
return result;
}
if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) {
result.err = 'Unknown algorithm';
return result;
}
// Parse URI
if (typeof uri === 'string') {
uri = hawk.utils.parseUri(uri);
}
// Calculate signature
var artifacts = {
ts: timestamp,
nonce: options.nonce || hawk.utils.randomString(6),
method: method,
resource: uri.relative,
host: uri.hostname,
port: uri.port,
hash: options.hash,
ext: options.ext,
app: options.app,
dlg: options.dlg
};
result.artifacts = artifacts;
// Calculate payload hash
if (!artifacts.hash &&
options.hasOwnProperty('payload')) {
artifacts.hash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
}
var mac = hawk.crypto.calculateMac('header', credentials, artifacts);
// Construct header
var hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''; // Other falsey values allowed
var header = 'Hawk id="' + credentials.id +
'", ts="' + artifacts.ts +
'", nonce="' + artifacts.nonce +
(artifacts.hash ? '", hash="' + artifacts.hash : '') +
(hasExt ? '", ext="' + hawk.utils.escapeHeaderAttribute(artifacts.ext) : '') +
'", mac="' + mac + '"';
if (artifacts.app) {
header += ', app="' + artifacts.app +
(artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"';
}
result.field = header;
return result;
},
// Validate server response
/*
request: object created via 'new XMLHttpRequest()' after response received
artifacts: object recieved from header().artifacts
options: {
payload: optional payload received
required: specifies if a Server-Authorization header is required. Defaults to 'false'
}
*/
authenticate: function (request, credentials, artifacts, options) {
options = options || {};
if (request.getResponseHeader('www-authenticate')) {
// Parse HTTP WWW-Authenticate header
var attributes = hawk.utils.parseAuthorizationHeader(request.getResponseHeader('www-authenticate'), ['ts', 'tsm', 'error']);
if (!attributes) {
return false;
}
if (attributes.ts) {
var tsm = hawk.crypto.calculateTsMac(attributes.ts, credentials);
if (tsm !== attributes.tsm) {
return false;
}
hawk.utils.setNtpOffset(attributes.ts - Math.floor((new Date()).getTime() / 1000)); // Keep offset at 1 second precision
}
}
// Parse HTTP Server-Authorization header
if (!request.getResponseHeader('server-authorization') &&
!options.required) {
return true;
}
var attributes = hawk.utils.parseAuthorizationHeader(request.getResponseHeader('server-authorization'), ['mac', 'ext', 'hash']);
if (!attributes) {
return false;
}
var modArtifacts = {
ts: artifacts.ts,
nonce: artifacts.nonce,
method: artifacts.method,
resource: artifacts.resource,
host: artifacts.host,
port: artifacts.port,
hash: attributes.hash,
ext: attributes.ext,
app: artifacts.app,
dlg: artifacts.dlg
};
var mac = hawk.crypto.calculateMac('response', credentials, modArtifacts);
if (mac !== attributes.mac) {
return false;
}
if (!options.hasOwnProperty('payload')) {
return true;
}
if (!attributes.hash) {
return false;
}
var calculatedHash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, request.getResponseHeader('content-type'));
return (calculatedHash === attributes.hash);
},
message: function (host, port, message, options) {
// Validate inputs
if (!host || typeof host !== 'string' ||
!port || typeof port !== 'number' ||
message === null || message === undefined || typeof message !== 'string' ||
!options || typeof options !== 'object') {
return null;
}
// Application time
var timestamp = options.timestamp || Math.floor((hawk.utils.now() + (options.localtimeOffsetMsec || 0)) / 1000)
// Validate credentials
var credentials = options.credentials;
if (!credentials ||
!credentials.id ||
!credentials.key ||
!credentials.algorithm) {
// Invalid credential object
return null;
}
if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) {
return null;
}
// Calculate signature
var artifacts = {
ts: timestamp,
nonce: options.nonce || hawk.utils.randomString(6),
host: host,
port: port,
hash: hawk.crypto.calculatePayloadHash(message, credentials.algorithm)
};
// Construct authorization
var result = {
id: credentials.id,
ts: artifacts.ts,
nonce: artifacts.nonce,
hash: artifacts.hash,
mac: hawk.crypto.calculateMac('message', credentials, artifacts)
};
return result;
},
authenticateTimestamp: function (message, credentials, updateClock) { // updateClock defaults to true
var tsm = hawk.crypto.calculateTsMac(message.ts, credentials);
if (tsm !== message.tsm) {
return false;
}
if (updateClock !== false) {
hawk.utils.setNtpOffset(message.ts - Math.floor((new Date()).getTime() / 1000)); // Keep offset at 1 second precision
}
return true;
}
};
hawk.crypto = {
headerVersion: '1',
algorithms: ['sha1', 'sha256'],
calculateMac: function (type, credentials, options) {
var normalized = hawk.crypto.generateNormalizedString(type, options);
var hmac = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()](normalized, credentials.key);
return hmac.toString(CryptoJS.enc.Base64);
},
generateNormalizedString: function (type, options) {
var normalized = 'hawk.' + hawk.crypto.headerVersion + '.' + type + '\n' +
options.ts + '\n' +
options.nonce + '\n' +
(options.method || '').toUpperCase() + '\n' +
(options.resource || '') + '\n' +
options.host.toLowerCase() + '\n' +
options.port + '\n' +
(options.hash || '') + '\n';
if (options.ext) {
normalized += options.ext.replace('\\', '\\\\').replace('\n', '\\n');
}
normalized += '\n';
if (options.app) {
normalized += options.app + '\n' +
(options.dlg || '') + '\n';
}
return normalized;
},
calculatePayloadHash: function (payload, algorithm, contentType) {
var hash = CryptoJS.algo[algorithm.toUpperCase()].create();
hash.update('hawk.' + hawk.crypto.headerVersion + '.payload\n');
hash.update(hawk.utils.parseContentType(contentType) + '\n');
hash.update(payload || '');
hash.update('\n');
return hash.finalize().toString(CryptoJS.enc.Base64);
},
calculateTsMac: function (ts, credentials) {
var hash = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()]('hawk.' + hawk.crypto.headerVersion + '.ts\n' + ts + '\n', credentials.key);
return hash.toString(CryptoJS.enc.Base64);
}
};
hawk.utils = {
storage: { // localStorage compatible interface
_cache: {},
setItem: function (key, value) {
hawk.utils.storage._cache[key] = value;
},
getItem: function (key) {
return hawk.utils.storage._cache[key];
}
},
setStorage: function (storage) {
var ntpOffset = hawk.utils.getNtpOffset() || 0;
hawk.utils.storage = storage;
hawk.utils.setNtpOffset(ntpOffset);
},
setNtpOffset: function (offset) {
try {
hawk.utils.storage.setItem('hawk_ntp_offset', offset);
}
catch (err) {
console.error('[hawk] could not write to storage.');
console.error(err);
}
},
getNtpOffset: function () {
return parseInt(hawk.utils.storage.getItem('hawk_ntp_offset') || '0', 10);
},
now: function () {
return (new Date()).getTime() + hawk.utils.getNtpOffset();
},
escapeHeaderAttribute: function (attribute) {
return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"');
},
parseContentType: function (header) {
if (!header) {
return '';
}
return header.split(';')[0].replace(/^\s+|\s+$/g, '').toLowerCase();
},
parseAuthorizationHeader: function (header, keys) {
if (!header) {
return null;
}
var headerParts = header.match(/^(\w+)(?:\s+(.*))?$/); // Header: scheme[ something]
if (!headerParts) {
return null;
}
var scheme = headerParts[1];
if (scheme.toLowerCase() !== 'hawk') {
return null;
}
var attributesString = headerParts[2];
if (!attributesString) {
return null;
}
var attributes = {};
var verify = attributesString.replace(/(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, function ($0, $1, $2) {
// Check valid attribute names
if (keys.indexOf($1) === -1) {
return;
}
// Allowed attribute value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9
if ($2.match(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/) === null) {
return;
}
// Check for duplicates
if (attributes.hasOwnProperty($1)) {
return;
}
attributes[$1] = $2;
return '';
});
if (verify !== '') {
return null;
}
return attributes;
},
randomString: function (size) {
var randomSource = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var len = randomSource.length;
var result = [];
for (var i = 0; i < size; ++i) {
result[i] = randomSource[Math.floor(Math.random() * len)];
}
return result.join('');
},
parseUri: function (input) {
// Based on: parseURI 1.2.2
// http://blog.stevenlevithan.com/archives/parseuri
// (c) Steven Levithan <stevenlevithan.com>
// MIT License
var keys = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'hostname', 'port', 'resource', 'relative', 'pathname', 'directory', 'file', 'query', 'fragment'];
var uriRegex = /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?(((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?)(?:#(.*))?)/;
var uriByNumber = uriRegex.exec(input);
var uri = {};
var i = 15;
while (i--) {
uri[keys[i]] = uriByNumber[i] || '';
}
if (uri.port === null ||
uri.port === '') {
uri.port = (uri.protocol.toLowerCase() === 'http' ? '80' : (uri.protocol.toLowerCase() === 'https' ? '443' : ''));
}
return uri;
}
};
// Based on: Crypto-JS v3.1.2
// Copyright (c) 2009-2013, Jeff Mott. All rights reserved.
// http://code.google.com/p/crypto-js/
// http://code.google.com/p/crypto-js/wiki/License
var CryptoJS=CryptoJS||function(h,r){var k={},l=k.lib={},n=function(){},f=l.Base={extend:function(a){n.prototype=this;var b=new n;a&&b.mixIn(a);b.hasOwnProperty("init")||(b.init=function(){b.$super.init.apply(this,arguments)});b.init.prototype=b;b.$super=this;return b},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var b in a)a.hasOwnProperty(b)&&(this[b]=a[b]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},j=l.WordArray=f.extend({init:function(a,b){a=this.words=a||[];this.sigBytes=b!=r?b:4*a.length},toString:function(a){return(a||s).stringify(this)},concat:function(a){var b=this.words,d=a.words,c=this.sigBytes;a=a.sigBytes;this.clamp();if(c%4)for(var e=0;e<a;e++)b[c+e>>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((c+e)%4);else if(65535<d.length)for(e=0;e<a;e+=4)b[c+e>>>2]=d[e>>>2];else b.push.apply(b,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,b=this.sigBytes;a[b>>>2]&=4294967295<<32-8*(b%4);a.length=h.ceil(b/4)},clone:function(){var a=f.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var b=[],d=0;d<a;d+=4)b.push(4294967296*h.random()|0);return new j.init(b,a)}}),m=k.enc={},s=m.Hex={stringify:function(a){var b=a.words;a=a.sigBytes;for(var d=[],c=0;c<a;c++){var e=b[c>>>2]>>>24-8*(c%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var b=a.length,d=[],c=0;c<b;c+=2)d[c>>>3]|=parseInt(a.substr(c,2),16)<<24-4*(c%8);return new j.init(d,b/2)}},p=m.Latin1={stringify:function(a){var b=a.words;a=a.sigBytes;for(var d=[],c=0;c<a;c++)d.push(String.fromCharCode(b[c>>>2]>>>24-8*(c%4)&255));return d.join("")},parse:function(a){for(var b=a.length,d=[],c=0;c<b;c++)d[c>>>2]|=(a.charCodeAt(c)&255)<<24-8*(c%4);return new j.init(d,b)}},t=m.Utf8={stringify:function(a){try{return decodeURIComponent(escape(p.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return p.parse(unescape(encodeURIComponent(a)))}},q=l.BufferedBlockAlgorithm=f.extend({reset:function(){this._data=new j.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=t.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,d=b.words,c=b.sigBytes,e=this.blockSize,f=c/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;c=h.min(4*a,c);if(a){for(var g=0;g<a;g+=e)this._doProcessBlock(d,g);g=d.splice(0,a);b.sigBytes-=c}return new j.init(g,c)},clone:function(){var a=f.clone.call(this);a._data=this._data.clone();return a},_minBufferSize:0});l.Hasher=q.extend({cfg:f.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){q.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(b,d){return(new a.init(d)).finalize(b)}},_createHmacHelper:function(a){return function(b,d){return(new u.HMAC.init(a,d)).finalize(b)}}});var u=k.algo={};return k}(Math);
(function () { var k = CryptoJS, b = k.lib, m = b.WordArray, l = b.Hasher, d = [], b = k.algo.SHA1 = l.extend({ _doReset: function () { this._hash = new m.init([1732584193, 4023233417, 2562383102, 271733878, 3285377520]) }, _doProcessBlock: function (n, p) { for (var a = this._hash.words, e = a[0], f = a[1], h = a[2], j = a[3], b = a[4], c = 0; 80 > c; c++) { if (16 > c) d[c] = n[p + c] | 0; else { var g = d[c - 3] ^ d[c - 8] ^ d[c - 14] ^ d[c - 16]; d[c] = g << 1 | g >>> 31 } g = (e << 5 | e >>> 27) + b + d[c]; g = 20 > c ? g + ((f & h | ~f & j) + 1518500249) : 40 > c ? g + ((f ^ h ^ j) + 1859775393) : 60 > c ? g + ((f & h | f & j | h & j) - 1894007588) : g + ((f ^ h ^ j) - 899497514); b = j; j = h; h = f << 30 | f >>> 2; f = e; e = g } a[0] = a[0] + e | 0; a[1] = a[1] + f | 0; a[2] = a[2] + h | 0; a[3] = a[3] + j | 0; a[4] = a[4] + b | 0 }, _doFinalize: function () { var b = this._data, d = b.words, a = 8 * this._nDataBytes, e = 8 * b.sigBytes; d[e >>> 5] |= 128 << 24 - e % 32; d[(e + 64 >>> 9 << 4) + 14] = Math.floor(a / 4294967296); d[(e + 64 >>> 9 << 4) + 15] = a; b.sigBytes = 4 * d.length; this._process(); return this._hash }, clone: function () { var b = l.clone.call(this); b._hash = this._hash.clone(); return b } }); k.SHA1 = l._createHelper(b); k.HmacSHA1 = l._createHmacHelper(b) })();
(function (k) { for (var g = CryptoJS, h = g.lib, v = h.WordArray, j = h.Hasher, h = g.algo, s = [], t = [], u = function (q) { return 4294967296 * (q - (q | 0)) | 0 }, l = 2, b = 0; 64 > b;) { var d; a: { d = l; for (var w = k.sqrt(d), r = 2; r <= w; r++) if (!(d % r)) { d = !1; break a } d = !0 } d && (8 > b && (s[b] = u(k.pow(l, 0.5))), t[b] = u(k.pow(l, 1 / 3)), b++); l++ } var n = [], h = h.SHA256 = j.extend({ _doReset: function () { this._hash = new v.init(s.slice(0)) }, _doProcessBlock: function (q, h) { for (var a = this._hash.words, c = a[0], d = a[1], b = a[2], k = a[3], f = a[4], g = a[5], j = a[6], l = a[7], e = 0; 64 > e; e++) { if (16 > e) n[e] = q[h + e] | 0; else { var m = n[e - 15], p = n[e - 2]; n[e] = ((m << 25 | m >>> 7) ^ (m << 14 | m >>> 18) ^ m >>> 3) + n[e - 7] + ((p << 15 | p >>> 17) ^ (p << 13 | p >>> 19) ^ p >>> 10) + n[e - 16] } m = l + ((f << 26 | f >>> 6) ^ (f << 21 | f >>> 11) ^ (f << 7 | f >>> 25)) + (f & g ^ ~f & j) + t[e] + n[e]; p = ((c << 30 | c >>> 2) ^ (c << 19 | c >>> 13) ^ (c << 10 | c >>> 22)) + (c & d ^ c & b ^ d & b); l = j; j = g; g = f; f = k + m | 0; k = b; b = d; d = c; c = m + p | 0 } a[0] = a[0] + c | 0; a[1] = a[1] + d | 0; a[2] = a[2] + b | 0; a[3] = a[3] + k | 0; a[4] = a[4] + f | 0; a[5] = a[5] + g | 0; a[6] = a[6] + j | 0; a[7] = a[7] + l | 0 }, _doFinalize: function () { var d = this._data, b = d.words, a = 8 * this._nDataBytes, c = 8 * d.sigBytes; b[c >>> 5] |= 128 << 24 - c % 32; b[(c + 64 >>> 9 << 4) + 14] = k.floor(a / 4294967296); b[(c + 64 >>> 9 << 4) + 15] = a; d.sigBytes = 4 * b.length; this._process(); return this._hash }, clone: function () { var b = j.clone.call(this); b._hash = this._hash.clone(); return b } }); g.SHA256 = j._createHelper(h); g.HmacSHA256 = j._createHmacHelper(h) })(Math);
(function(){var c=CryptoJS,k=c.enc.Utf8;c.algo.HMAC=c.lib.Base.extend({init:function(a,b){a=this._hasher=new a.init;"string"==typeof b&&(b=k.parse(b));var c=a.blockSize,e=4*c;b.sigBytes>e&&(b=a.finalize(b));b.clamp();for(var f=this._oKey=b.clone(),g=this._iKey=b.clone(),h=f.words,j=g.words,d=0;d<c;d++)h[d]^=1549556828,j[d]^=909522486;f.sigBytes=g.sigBytes=e;this.reset()},reset:function(){var a=this._hasher;a.reset();a.update(this._iKey)},update:function(a){this._hasher.update(a);return this},finalize:function(a){var b=this._hasher;a=b.finalize(a);b.reset();return b.finalize(this._oKey.clone().concat(a))}})})();
(function(){var h=CryptoJS,j=h.lib.WordArray;h.enc.Base64={stringify:function(b){var e=b.words,f=b.sigBytes,c=this._map;b.clamp();b=[];for(var a=0;a<f;a+=3)for(var d=(e[a>>>2]>>>24-8*(a%4)&255)<<16|(e[a+1>>>2]>>>24-8*((a+1)%4)&255)<<8|e[a+2>>>2]>>>24-8*((a+2)%4)&255,g=0;4>g&&a+0.75*g<f;g++)b.push(c.charAt(d>>>6*(3-g)&63));if(e=c.charAt(64))for(;b.length%4;)b.push(e);return b.join("")},parse:function(b){var e=b.length,f=this._map,c=f.charAt(64);c&&(c=b.indexOf(c),-1!=c&&(e=c));for(var c=[],a=0,d=0;d<e;d++)if(d%4){var g=f.indexOf(b.charAt(d-1))<<2*(d%4),h=f.indexOf(b.charAt(d))>>>6-2*(d%4);c[a>>>2]|=(g|h)<<24-8*(a%4);a++}return j.create(c,a)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})();

View file

@ -0,0 +1,84 @@
/* global hawk */
/* global Config */
/* global DUMP */
'use strict';
var Requester = {
XHR_TIMEOUT_MS: 60000,
_url: null,
_hawkCredentials: null,
init: function fmdr_init() {
this._url = Config.api_url + '/' + Config.api_version;
},
setHawkCredentials: function fmdr_set_hawk_credentials(id, key) {
this._hawkCredentials = {
id: id,
key: key,
algorithm: 'sha256'
};
},
post: function fmdr_post(url, data, onsuccess, onerror) {
url = this._url + url;
data = JSON.stringify(data);
DUMP('POST-ing to ' + url + ': ' + data);
var xhr = new XMLHttpRequest({mozSystem: true});
xhr.open('POST', url);
xhr.timeout = this.XHR_TIMEOUT_MS;
xhr.setRequestHeader('Content-Type', 'application/json');
var hawkHeader = null;
if (this._hawkCredentials) {
var hawkOptions = {
credentials: this._hawkCredentials,
contentType: 'application/json',
payload: data
};
hawkHeader = hawk.client.header(url, 'POST', hawkOptions);
xhr.setRequestHeader('Authorization', hawkHeader.field);
}
xhr.onload = function fmdr_xhr_onload() {
var valid = true;
if (hawkHeader !== null) {
valid = hawk.client.authenticate(
xhr, this._hawkCredentials, hawkHeader.artifacts,
{payload: xhr.responseText});
}
if (!valid) {
DUMP('ignoring invalid HAWK signature');
return;
}
if (xhr.status == 200) {
DUMP('successful request, response: ' + xhr.response);
onsuccess && onsuccess(JSON.parse(xhr.responseText));
} else if (xhr.status !== 200) {
DUMP('request failed with status ' + xhr.status);
onerror && onerror(xhr);
}
};
xhr.onerror = function fmd_xhr_onerror() {
DUMP('request failed with status ' + xhr.status);
onerror && onerror(xhr);
};
xhr.ontimeout = function fmd_xhr_ontimeout() {
DUMP('server request timed out!');
xhr.onerror();
};
xhr.send(data);
}
};
Requester.init();

View file

@ -0,0 +1,18 @@
{
"name": "findmydevice",
"tags": {
"dictionaries": ["jsdoc"]
},
"source": {
"include": ["apps/findmydevice/js/**/*.js"]
},
"plugins": [
"plugins/markdown",
"plugins/summarize"
],
"opts": {
"private": true,
"recurse": true,
"destination": "docs/findmydevice"
}
}

View file

@ -0,0 +1,89 @@
{
"name": "Find My Device",
"type": "certified",
"role": "system",
"description": "Find My Device helps find, lock and erase a lost phone",
"launch_path": "/index.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"permissions": {
"themeable": {},
"alarms": {},
"audio-channel-ringer": {},
"device-storage:sdcard": {
"access": "readwrite"
},
"device-storage:pictures": {
"access": "readwrite"
},
"device-storage:videos": {
"access": "readwrite"
},
"device-storage:music": {
"access": "readwrite"
},
"device-storage:crashes": {
"access": "readwrite"
},
"firefox-accounts": {},
"geolocation-noprompt": {},
"killswitch": {},
"power": {},
"push": {},
"settings": {
"access": "readwrite"
},
"systemXHR": {},
"moz-extremely-unstable-and-will-change-webcomponents": {}
},
"locales": {
"en-US": {
"name": "Find My Device",
"description": "Find My Device helps find, lock and erase a lost phone"
},
"fr": {
"name": "Localiser mon appareil",
"description": "Localiser mon appareil vous permet de retrouver, verrouiller ou effacer un téléphone égaré"
}
},
"default_locale": "en-US",
"icons": {
"84": "/style/icons/find_my_device_84.png",
"126": "/style/icons/find_my_device_126.png",
"142": "/style/icons/find_my_device_142.png",
"189": "/style/icons/find_my_device_189.png",
"284": "/style/icons/find_my_device_284.png"
},
"messages": [
{
"alarm": "/index.html"
},
{
"push": "/index.html"
},
{
"push-register": "/index.html"
}
],
"connections": {
"findmydevice-wakeup": {
"description": "Wake up Find My Device from the System app",
"rules": {
"manifestURLs": [
"app://system.gaiamobile.org/manifest.webapp",
"app://settings.gaiamobile.org/manifest.webapp"
]
}
},
"findmydevice-test": {
"description": "Manually trigger commands for testing",
"rules": {
"manifestURLs": [
"app://test-findmydevice.gaiamobile.org/manifest.webapp"
]
}
}
}
}

View file

@ -0,0 +1,10 @@
{
"webide": {
"prepackage": {
"command": "make",
"env": ["APP=findmydevice"],
"cwd": "../.."
},
"packageDir": "../../build_stage/findmydevice/"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,27 @@
'use strict';
/* jshint node: true, mocha: true */
/* global suiteSetup */
var helper = require('helper');
suite('Findmydevice tests', function() {
suiteSetup(helper.cleanupWorkspace);
teardown(helper.cleanupWorkspace);
test('APP=findmydevice make', function(done) {
helper.exec('APP=findmydevice make', function(error, stdout, stderr) {
helper.checkError(error, stdout, stderr);
var zipPath = process.cwd() +
'/profile/webapps/findmydevice.gaiamobile.org/application.zip';
var checkList = ['manifest.webapp'];
checkList.forEach(function(filePath) {
helper.checkFilePathInZip(zipPath, filePath);
});
done();
});
});
});

View file

@ -0,0 +1,17 @@
{
"extends": "../../../../.jshintrc",
"predef": [
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"marionette",
"marionetteScriptFinished",
"require",
"define",
"requireApp",
"sinon"
]
}

View file

@ -0,0 +1,106 @@
'use strict';
/* global __dirname */
marionette('Find My Device lock >', function() {
var assert = require('assert');
var FINDMYDEVICE_TEST_APP = 'app://test-findmydevice.gaiamobile.org';
var SETTINGS_DIGEST_VALUE = 'lockscreen.passcode-lock.digest.value';
var client = marionette.client({
profile: {
apps: {
'test-findmydevice.gaiamobile.org':
__dirname + '/fixtures/test-findmydevice',
}
}
});
setup(function() {
// launch the test app
client.apps.launch(FINDMYDEVICE_TEST_APP);
client.apps.switchToApp(FINDMYDEVICE_TEST_APP);
client.helper.waitForElement('body');
});
test('Lock the screen through the test app', function() {
var messageText = 'This phone is lost.';
var messageInput = client.findElement('input[name="message"]');
messageInput.sendKeys(messageText);
client.switchToFrame();
var oldDigest = client.settings.get(SETTINGS_DIGEST_VALUE);
client.apps.switchToApp(FINDMYDEVICE_TEST_APP);
var passcode = '4567';
var passcodeInput = client.findElement('input[name="code"]');
passcodeInput.sendKeys(passcode);
var lockButton = client.findElement('#lock');
lockButton.click();
// XXX: After we make LockScreen as an iframe or app, this should be the
// line to switch to LockScreen frame.
client.switchToFrame();
var lockscreen = client.findElement('#lockscreen');
client.waitFor(function() {
return lockscreen.displayed();
});
var lockscreenMessage = client.findElement('#lockscreen-message');
client.waitFor(function() {
var text = lockscreenMessage.text();
if (text !== '') {
assert.equal(lockscreenMessage.text(), messageText);
return true;
}
return false;
});
// sanity-check the settings we should have set
var settings = {
'lockscreen.enabled': true,
'lockscreen.passcode-lock.enabled': true,
'lockscreen.notifications-preview.enabled': false,
'lockscreen.lock-message': messageText,
};
for (var s in settings) {
assert.equal(client.settings.get(s), settings[s]);
}
// checking if passcode was changed:
var newDigest = client.settings.get(SETTINGS_DIGEST_VALUE);
assert.notEqual(newDigest, oldDigest);
client.switchToFrame();
// now unlock the screen and re-lock it, the message should disappear
client.executeScript(function() {
window.wrappedJSObject.Service.request('unlock');
});
client.waitFor(function() {
return client.executeScript(function() {
return !window.wrappedJSObject.Service.locked;
});
});
client.executeScript(function() {
window.wrappedJSObject.Service.request('lock');
});
// XXX: After we make LockScreen as an iframe or app, this should be the
// line to switch to LockScreen frame.
client.switchToFrame();
lockscreen = client.findElement('#lockscreen');
client.waitFor(function() {
return lockscreen.displayed();
});
lockscreenMessage = client.findElement('#lockscreen-message');
client.waitFor(function() {
return !lockscreenMessage.displayed();
});
assert.equal(client.settings.get('lockscreen.lock-message'), '');
});
});

View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<title>Find My Device Test App</title>
<style>
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
label {
display: block;
}
</style>
<script src="js/findmydevice.js"></script>
</head>
<body>
<div id="buttons">
<button id="track">Track</button>
<button id="erase">Erase</button>
<button id="lock">Lock</button>
<button id="ring">Ring</button>
</div>
<div id="options">
<label>Duration <input name="duration" type="number" placeholder="60"/></label>
<label>Message <input name="message" type="text" placeholder="Help! I'm lost!"/></label>
<label>Passcode <input name="code" type="text" placeholder="0000"/></label>
</div>
</body>
</html>

View file

@ -0,0 +1,32 @@
'use strict';
window.onload = function() {
var systemPort = null;
navigator.mozApps.getSelf().onsuccess = function() {
var app = this.result;
app.connect('findmydevice-test').then(function(ports) {
if (ports.length !== 1) {
console.error('got more than one connection?');
return;
}
systemPort = ports[0];
}, function(err) {
console.error('failed to connect: ' + err);
});
};
var buttons = document.getElementById('buttons');
buttons.addEventListener('click', function(event) {
var cmdobj = {};
var command = event.target.id[0];
cmdobj[command] = {};
var options = document.getElementsByTagName('input');
for (var i = 0; i < options.length; i++) {
cmdobj[command][options[i].name[0]] = options[i].value;
}
systemPort.postMessage(cmdobj);
});
};

View file

@ -0,0 +1,14 @@
{
"name": "Test Find My Device",
"description": "Manually issue commands for Find My Device",
"type": "certified",
"launch_path": "/index.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"default_locale": "en-US",
"icons": {
"128": "/style/icons/Blank.png"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,17 @@
{
"extends": "../../../../.jshintrc",
"predef": [
"assert",
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"require",
"requireApp",
"sinon",
"mocha",
"loadBodyHTML"
]
}

View file

@ -0,0 +1,506 @@
/* global MocksHelper */
/* global MockSettingsListener */
/* global MockGeolocation */
/* global Commands */
/* global FindMyDevice */
/* global MockNavigatorSettings */
/* global PasscodeHelper */
'use strict';
require('/shared/test/unit/mocks/mocks_helper.js');
require('/shared/test/unit/mocks/mock_settings_url.js');
require('/shared/test/unit/mocks/mock_navigator_moz_settings.js');
require('/shared/test/unit/mocks/mock_settings_listener.js');
require('/shared/test/unit/mocks/mock_settings_helper.js');
require('/shared/test/unit/mocks/mock_audio.js');
require('/shared/test/unit/mocks/mock_device_storage.js');
require('/shared/test/unit/mocks/mock_geolocation.js');
require('/shared/js/passcode_helper.js');
var mocksForFindMyDevice = new MocksHelper([
'SettingsListener', 'SettingsURL', 'SettingsHelper', 'Audio',
'DeviceStorage', 'Geolocation'
]).init();
suite('FindMyDevice >', function() {
var realMozPower;
var realKillSwitch;
var realMozApps;
var fakeClock;
var realMozSettings;
mocksForFindMyDevice.attachTestHelpers();
var subject;
setup(function(done) {
realMozPower = navigator.mozPower;
navigator.mozPower = {
factoryReset: function(reason) {},
};
this.sinon.spy(navigator.mozPower, 'factoryReset');
realKillSwitch = navigator.mozKillSwitch;
navigator.mozKillSwitch = {
enable: function() {}
};
realMozApps = navigator.mozApps;
navigator.mozApps = {
getSelf: function() {
var app = {
manifestURL: 'app://findmydevice.gaiamobile.org/manifest.webapp',
origin: 'app://findmydevice.gaiamobile.org/'
};
var request = {result: app};
setTimeout(function() {
request.onsuccess.call(request);
});
return request;
}
};
// replace shared/js/dump.js
window.DUMP = function() {};
fakeClock = this.sinon.useFakeTimers();
realMozSettings = navigator.mozSettings;
navigator.mozSettings = MockNavigatorSettings;
window.FindMyDevice = {
beginHighPriority: this.sinon.stub(),
endHighPriority: this.sinon.stub()
};
require('/js/commands.js', function() {
subject = Commands;
done();
});
});
suite('Ring command', function() {
var duration = 2;
setup(function() {
this.sinon.spy(window, 'setTimeout');
this.sinon.spy(window, 'clearTimeout');
});
test('Basic invocation', function() {
var ringtone = 'user selected ringtone';
MockSettingsListener.mCallbacks['dialer.ringtone'](ringtone);
Commands.invokeCommand('ring', [duration, function(retval) {
var lock = MockSettingsListener.getSettingsLock().locks.pop();
var channel = Commands._ringer.mozAudioChannelType;
assert.equal(channel, 'ringer', 'use notification channel');
assert.equal(lock['audio.volume.notification'], 15,
'volume set to maximum');
assert.equal(Commands._ringer.paused, false, 'must be playing');
assert.equal(Commands._ringer.src, ringtone, 'must use ringtone');
sinon.assert.calledOnce(FindMyDevice.beginHighPriority);
sinon.assert.calledWith(FindMyDevice.beginHighPriority, 'command');
}]);
fakeClock.tick(duration * 1000);
assert.equal(Commands._ringer.paused, true, 'must have stopped');
sinon.assert.calledOnce(FindMyDevice.endHighPriority);
sinon.assert.calledWith(FindMyDevice.endHighPriority, 'command');
});
test('Stop ringing if duration is zero', function() {
Commands.invokeCommand('ring', [duration, function(retval) {
assert.equal(Commands._ringer.paused, false, 'must be playing');
sinon.assert.called(window.setTimeout);
sinon.assert.calledOnce(FindMyDevice.beginHighPriority);
sinon.assert.notCalled(FindMyDevice.endHighPriority);
}]);
fakeClock.tick(duration/2 * 1000);
var timeoutId = Commands._ringTimeoutId;
assert.isFalse(Commands._ringer.paused, 'must not be done ringing');
Commands.invokeCommand('ring', [0, function(retval) {}]);
assert.equal(Commands._ringer.paused, true, 'must have stopped');
assert.isNull(Commands._ringTimeoutId, 'timeout must have been canceled');
sinon.assert.calledWith(window.clearTimeout, timeoutId);
sinon.assert.calledTwice(FindMyDevice.beginHighPriority);
sinon.assert.alwaysCalledWith(FindMyDevice.beginHighPriority, 'command');
sinon.assert.calledTwice(FindMyDevice.endHighPriority);
sinon.assert.alwaysCalledWith(FindMyDevice.endHighPriority, 'command');
});
test('Should do nothing if already ringing', function() {
Commands.invokeCommand('ring', [duration, function(retval) {
assert.isFalse(Commands._ringer.paused, 'must be playing');
sinon.assert.calledOnce(FindMyDevice.beginHighPriority);
sinon.assert.notCalled(FindMyDevice.endHighPriority);
}]);
fakeClock.tick(duration/2 * 1000);
assert.isFalse(Commands._ringer.paused, 'must not be done ringing');
Commands.invokeCommand('ring', [duration, function(retval) {}]);
fakeClock.tick(duration/2 * 1000);
assert.isTrue(Commands._ringer.paused, 'must have stopped');
assert.isNull(Commands._ringTimeoutId, 'no timeouts should be set');
sinon.assert.calledTwice(FindMyDevice.beginHighPriority);
sinon.assert.alwaysCalledWith(FindMyDevice.beginHighPriority, 'command');
sinon.assert.calledTwice(FindMyDevice.endHighPriority);
sinon.assert.alwaysCalledWith(FindMyDevice.endHighPriority, 'command');
});
test('Should do nothing if not ringing and duration is zero', function() {
Commands.invokeCommand('ring', [0, function(retval) {}]);
assert.isTrue(Commands._ringer.paused, 'must not have started');
assert.isNull(Commands._ringTimeoutId);
sinon.assert.calledOnce(FindMyDevice.beginHighPriority);
sinon.assert.alwaysCalledWith(FindMyDevice.beginHighPriority, 'command');
sinon.assert.calledOnce(FindMyDevice.endHighPriority);
sinon.assert.alwaysCalledWith(FindMyDevice.endHighPriority, 'command');
});
});
test('Erase: ensure factoryReset is called with "wipe"', function(done) {
subject.invokeCommand('erase', [function(retval, error) {
assert.equal(navigator.mozPower.factoryReset.calledWith('wipe'), true);
done();
}]);
});
suite('Track command', function() {
test('Track receives positions correctly', function() {
// track for a period of time
var duration = (subject.TRACK_UPDATE_INTERVAL_MS) / 1000;
subject.invokeCommand('track', [duration, function(retval, position) {
assert.equal(retval, true);
// ensure, while tracking, the locations match what the mock makes
assert.deepEqual(position, MockGeolocation.fakePosition ,
'check the position is what the mock provided');
}]);
fakeClock.tick(2* subject.TRACK_UPDATE_INTERVAL_MS);
});
test('Track command', function() {
// track for 3 intervals
var duration = (3 * subject.TRACK_UPDATE_INTERVAL_MS) / 1000;
subject.invokeCommand('track', [duration, function(retval, position) {}]);
// after 2 intervals...
fakeClock.tick(2 * subject.TRACK_UPDATE_INTERVAL_MS);
// we're mid-track, check we have a watch and timeout IDs
assert.notEqual(subject._watchPositionId, null);
assert.notEqual(subject._trackTimeoutId, null);
// after another interval...
fakeClock.tick(1 * subject.TRACK_UPDATE_INTERVAL_MS);
// .. tracking should have stopped.
// ensure we no longer have track and watch IDs.
assert.equal(subject._trackTimeoutId, null);
assert.equal(subject._watchPositionId, null);
sinon.assert.alwaysCalledWith(FindMyDevice.beginHighPriority, 'command');
sinon.assert.alwaysCalledWith(FindMyDevice.endHighPriority, 'command');
// check wakelock count
var begin = FindMyDevice.beginHighPriority.callCount;
var end = FindMyDevice.beginHighPriority.callCount;
assert.equal(begin, end, 'begin and end count should match');
});
// We assume for this test that the interval used by the geolocation mock
// is significantly smaller than TRACK_UPDATE_INTERVAL.
test('Track command should throttle observed positions', function() {
// track for a long duration (e.g. 10 intervals)
var duration = 10 * subject.TRACK_UPDATE_INTERVAL_MS / 1000;
var positions = 0;
subject.invokeCommand('track', [duration, function(retval, position) {
positions++;
}]);
// set the clock to just before the interval, ensure no locations are seen
var amountBefore = 2;
fakeClock.tick(subject.TRACK_UPDATE_INTERVAL_MS - amountBefore);
assert.equal(positions, 0, 'no update intervals should have passed');
// set the clock to almost two intervals, ensure only one location is seen
fakeClock.tick(subject.TRACK_UPDATE_INTERVAL_MS);
assert.equal(positions, 1, '1 update interval should have passed');
});
test('Track command should update its duration if invoked while running',
function() {
// track for a long duration (e.g. 10 intervals)
var duration = 10 * subject.TRACK_UPDATE_INTERVAL_MS / 1000;
var positions = 0;
subject.invokeCommand('track', [duration, function(retval, position) {
positions++;
}]);
// ensure the position count updates as expected (one position per
// interval)
fakeClock.tick(subject.TRACK_UPDATE_INTERVAL_MS);
assert.equal(positions, 1, '1 update interval should have passed');
// reset the duration to a shorter value:
duration = 2 * subject.TRACK_UPDATE_INTERVAL_MS / 1000;
subject.invokeCommand('track', [duration, function(retval, position) {
positions++;
}]);
// allow the timer to progress to after the duration completes
fakeClock.tick(5 * subject.TRACK_UPDATE_INTERVAL_MS);
// ensure tracking stopped at the end of the new duration:
// position count should be at 3
assert.equal(positions, 3, '2 more (3) intervals should have passed');
// and the track and watch IDs should be null
assert.equal(subject._trackTimeoutId, null);
assert.equal(subject._watchPositionId, null);
// check wakelock count
var begin = FindMyDevice.beginHighPriority.callCount;
var end = FindMyDevice.beginHighPriority.callCount;
assert.equal(begin, end, 'begin and end count should match');
});
test('Track command should stop if duration is zero',
function() {
// track for a long duration (e.g. 10 intervals)
var duration = 10 * subject.TRACK_UPDATE_INTERVAL_MS / 1000;
var positions = 0;
subject.invokeCommand('track', [duration, function(retval, position) {
positions++;
}]);
fakeClock.tick(subject.TRACK_UPDATE_INTERVAL_MS);
// after a single interval, stop the tracking
subject.invokeCommand('track', [0, function(retval) {
assert.equal(retval, true);
}]);
// allow more intervals to pass
fakeClock.tick(2 * subject.TRACK_UPDATE_INTERVAL_MS);
// and ensure the position count remains at 1
assert.equal(positions, 1);
// check the track and watch IDs should be null
assert.equal(subject._trackTimeoutId, null);
assert.equal(subject._watchPositionId, null);
});
test('track should not leak wakelocks with nonzero duration', function() {
// start tracking
var duration = 9 * subject.TRACK_UPDATE_INTERVAL_MS / 1000;
subject.invokeCommand('track', [duration, function() {}]);
// once tracking is complete, check the wakelock call counts match
fakeClock.tick(10 * subject.TRACK_UPDATE_INTERVAL_MS);
var begin = FindMyDevice.beginHighPriority.callCount;
var end = FindMyDevice.beginHighPriority.callCount;
assert.equal(begin, end, 'begin and end count should match');
});
test('track should not leak wakelocks with zero duration', function() {
subject.invokeCommand('track', [0, function() {}]);
var begin = FindMyDevice.beginHighPriority.callCount;
var end = FindMyDevice.beginHighPriority.callCount;
assert.equal(begin, end, 'begin and end count should match');
});
test('track should not leak wakelocks if stopped mid-track', function() {
// track for a long duration (e.g. 10 intervals)
var duration = 10 * subject.TRACK_UPDATE_INTERVAL_MS / 1000;
subject.invokeCommand('track', [duration, function() {}]);
// after a single interval, stop the tracking
fakeClock.tick(subject.TRACK_UPDATE_INTERVAL_MS);
subject.invokeCommand('track', [0, function(retval) {}]);
// go past the original duration
fakeClock.tick(10 * subject.TRACK_UPDATE_INTERVAL_MS);
// check the wakelock call counts
var begin = FindMyDevice.beginHighPriority.callCount;
var end = FindMyDevice.beginHighPriority.callCount;
assert.equal(begin, end, 'begin and end count should match');
});
});
test('Bug 1027325 - correctly check that passcode lock is set', function() {
MockSettingsListener.mTriggerCallback('lockscreen.enabled', true);
MockSettingsListener.mTriggerCallback('lockscreen.passcode-lock.enabled',
'1234');
assert.equal(true, subject.deviceHasPasscode());
});
test('Bug 1027325 - correctly check that passcode lock is unset', function() {
MockSettingsListener.mTriggerCallback('lockscreen.enabled', false);
MockSettingsListener.mTriggerCallback('lockscreen.passcode-lock.enabled',
false);
assert.equal(false, subject.deviceHasPasscode());
});
test('Bug 1027325 - correctly check that lockscreen is set, but passcode ' +
'lock is unset', function() {
MockSettingsListener.mTriggerCallback('lockscreen.enabled', true);
MockSettingsListener.mTriggerCallback('lockscreen.passcode-lock.enabled',
false);
assert.equal(false, subject.deviceHasPasscode());
});
suite('KillSwitch command', function() {
suite('With API existing', function() {
setup(function() {
this.sinon.spy(navigator.mozKillSwitch, 'enable');
});
test('Basic invocation', function(done) {
subject.invokeCommand('killswitch', [ function(retval, error) {
sinon.assert.calledOnce(navigator.mozKillSwitch.enable);
sinon.assert.calledOnce(FindMyDevice.beginHighPriority);
sinon.assert.calledWith(FindMyDevice.beginHighPriority, 'command');
} ]);
sinon.assert.calledOnce(FindMyDevice.endHighPriority);
sinon.assert.calledWith(FindMyDevice.endHighPriority, 'command');
done();
});
});
suite('Without API existing', function() {
setup(function() {
navigator.mozKillSwitch = undefined;
});
test('No invocation', function(done) {
subject.invokeCommand('killswitch', [ function(retval, error) {
sinon.assert.calledOnce(FindMyDevice.beginHighPriority);
sinon.assert.calledWith(FindMyDevice.beginHighPriority, 'command');
} ]);
sinon.assert.calledOnce(FindMyDevice.endHighPriority);
sinon.assert.calledWith(FindMyDevice.endHighPriority, 'command');
done();
});
});
});
test('List of accepted commands', function() {
MockSettingsListener.mTriggerCallback('geolocation.enabled', true);
var allCommands = ['track', 'erase', 'ring', 'lock', 'killswitch'];
var enabledCommands = subject.getEnabledCommands();
assert.deepEqual(enabledCommands.sort(), allCommands.sort());
// track should be disabled when geolocation is disabled
MockSettingsListener.mTriggerCallback('geolocation.enabled', false);
allCommands = ['erase', 'ring', 'lock', 'killswitch'];
enabledCommands = subject.getEnabledCommands();
assert.deepEqual(enabledCommands.sort(), allCommands.sort());
});
teardown(function() {
navigator.mozPower = realMozPower;
navigator.mozKillSwitch = realKillSwitch;
navigator.mozApps = realMozApps;
navigator.mozSettings = realMozSettings;
delete window.DUMP;
fakeClock.restore();
delete window.FindMyDevice;
});
});
suite('FindMyDevice (with real clock) >', function() {
var realMozSettings;
mocksForFindMyDevice.attachTestHelpers();
var subject;
setup(function(done) {
// replace shared/js/dump.js
window.DUMP = function() {};
realMozSettings = navigator.mozSettings;
MockNavigatorSettings.mSetup();
navigator.mozSettings = MockNavigatorSettings;
window.FindMyDevice = {
beginHighPriority: this.sinon.stub(),
endHighPriority: this.sinon.stub()
};
require('/js/commands.js', function() {
subject = Commands;
done();
});
});
test('Lock command', function(done) {
var code = '1234', message = 'locked!';
var DIGEST_VALUE = 'lockscreen.passcode-lock.digest.value';
var settingsLock = MockSettingsListener.getSettingsLock();
var oldDigest = settingsLock.get(DIGEST_VALUE);
subject.invokeCommand('lock', [message, code, function(retval) {
assert.equal(retval, true);
sinon.assert.calledWith(FindMyDevice.beginHighPriority, 'command');
sinon.assert.calledWith(FindMyDevice.endHighPriority, 'command');
var lock = MockSettingsListener.getSettingsLock().locks.pop();
assert.deepEqual({
'lockscreen.enabled': true,
'lockscreen.notifications-preview.enabled': false,
'lockscreen.passcode-lock.enabled': true,
'lockscreen.lock-message': message,
'lockscreen.lock-immediately': true
}, lock, 'check that the correct settings were set');
assert.notEqual(oldDigest, lock[DIGEST_VALUE],
'check that Passcode digest changed');
done();
}]);
});
test('Bug 1201530 - check existing passcode override', function() {
var code = '2342', message = 'already locked!';
var stubPasscodeSet = sinon.stub(PasscodeHelper, 'set', function() {
return Promise.resolve(true);
});
// Simulate already-locked device.
MockSettingsListener.mTriggerCallback('lockscreen.enabled', true);
MockSettingsListener.mTriggerCallback('lockscreen.passcode-lock.enabled',
true);
subject.invokeCommand('lock', [message, code, function() {}]);
assert.isTrue(stubPasscodeSet.calledWith(code),
'existing passcode is not overridden');
stubPasscodeSet.restore();
});
teardown(function() {
// clean up sinon.js stubs
navigator.mozSettings = realMozSettings;
MockNavigatorSettings.mTeardown();
delete window.DUMP;
delete window.FindMyDevice;
});
});

View file

@ -0,0 +1,135 @@
/* global MocksHelper, hawk, Requester */
'use strict';
require('/shared/test/unit/mocks/mock_dump.js');
var mocksForFindMyDevice = new MocksHelper([
'Dump'
]).init();
suite('FindMyDevice >', function() {
var fakeXHR;
mocksForFindMyDevice.attachTestHelpers();
suiteSetup(function(done) {
window.Config = {
api_url: 'https://find.firefox.com',
api_version: 'v0'
};
// We require requester.js here and not above because
// we want to make sure all of our dependencies have already
// been loaded.
require('/js/requester.js', function() {
done();
});
});
suiteTeardown(function() {
delete window.Config;
});
var xhr;
setup(function() {
xhr = null;
fakeXHR = sinon.useFakeXMLHttpRequest();
fakeXHR.onCreate = function(request) {
xhr = request;
};
});
teardown(function() {
fakeXHR.restore();
});
test('url is properly loaded from configuration', function() {
assert.equal(Requester._url, 'https://find.firefox.com/v0');
});
test('XHR is created with the correct properties', function() {
Requester.post('/register');
assert.equal(xhr.url, 'https://find.firefox.com/v0/register');
assert.equal(xhr.requestHeaders['Content-Type'],
'application/json;charset=utf-8');
assert.equal(xhr.method, 'POST');
assert.equal(xhr.timeout, Requester.XHR_TIMEOUT_MS);
});
test('onsuccess is called on successful server response', function() {
var response = '{"ok": true}';
var onsuccess = this.sinon.stub();
Requester.post('/register', {}, onsuccess);
xhr.respond(200, {'Content-type': 'application/json'}, response);
sinon.assert.called(onsuccess);
});
test('onerror is called on failed server response', function() {
var onerror = this.sinon.stub();
Requester.post('/register', {}, null, onerror);
xhr.respond(401, {}, '');
sinon.assert.called(onerror);
});
test('onerror is called on timeout', function() {
var onerror = this.sinon.stub();
Requester.post('/register', {}, null, onerror);
xhr.ontimeout();
sinon.assert.called(onerror);
});
suite('HAWK credentials', function() {
var hawkOptions = {
credentials: {id: 'id', key: 'key', algorithm: 'sha256'},
contentType: 'application/json',
payload: '{}'
};
suiteSetup(function() {
window.hawk = {
client: {
header: function() {
return {field: 'header'};
}
}
};
Requester.setHawkCredentials('id', 'key');
});
suiteTeardown(function() {
delete window.hawk;
});
setup(function() {
this.sinon.spy(hawk.client, 'header');
});
test('HAWK credentials are included in the request', function() {
Requester.post('/register', {});
sinon.assert.calledWith(hawk.client.header,
xhr.url, 'POST', hawkOptions);
assert.equal(xhr.requestHeaders.Authorization,
hawk.client.header().field);
});
test('ignore response if HAWK doesn\'t verify', function() {
hawk.client.authenticate = function() {
return false;
};
var onsuccess = this.sinon.stub();
var onerror = this.sinon.stub();
Requester.post('/register', {}, onsuccess, onerror);
xhr.respond(200, {'Content-type': 'application/json'}, '{}');
sinon.assert.notCalled(onsuccess);
sinon.assert.notCalled(onerror);
});
});
});

View file

@ -0,0 +1,826 @@
/* global MocksHelper, MockGeolocation, MockNavigatormozSetMessageHandler,
MockSettingsHelper, MockNavigatorSettings, FindMyDevice, MockMozAlarms,
IAC_API_WAKEUP_REASON_LOGIN, IAC_API_WAKEUP_REASON_LOGOUT,
IAC_API_WAKEUP_REASON_TRY_DISABLE, IAC_API_WAKEUP_REASON_ENABLED_CHANGED,
IAC_API_WAKEUP_REASON_LOCKSCREEN_CLOSED, Commands
*/
'use strict';
require('/shared/test/unit/mocks/mocks_helper.js');
require('/shared/test/unit/mocks/mock_dump.js');
require('/shared/test/unit/mocks/mock_async_storage.js');
require('/shared/test/unit/mocks/mock_navigator_moz_settings.js');
require('/shared/test/unit/mocks/mock_settings_listener.js');
require('/shared/test/unit/mocks/mock_settings_helper.js');
require('/shared/test/unit/mocks/mock_geolocation.js');
require('/shared/test/unit/mocks/mock_navigator_moz_set_message_handler.js');
require('/shared/js/findmydevice_iac_api.js');
require('/shared/test/unit/mocks/mock_moz_alarms.js');
var mocksForFindMyDevice = new MocksHelper([
'asyncStorage',
'Dump',
'Geolocation',
'SettingsHelper'
]).init();
suite('FindMyDevice >', function() {
var realMozId;
var realMozSettings;
var realMozSetMessageHandler;
var realMozAlarms;
var realNavigatorOnLine;
var realCommands;
var observerSpy;
mocksForFindMyDevice.attachTestHelpers();
suiteSetup(function(done) {
realMozId = navigator.mozId;
// attempting to stub only the request method of mozId,
// as in |sinon.stub(navigator.mozId, 'request', ...)|,
// causes an exception to be raised, so we replace the
// entire mozId object.
navigator.mozId = {
request: sinon.stub(),
watch: sinon.stub()
};
realMozSettings = navigator.mozSettings;
navigator.mozSettings = MockNavigatorSettings;
MockNavigatorSettings.mSetup();
realMozSetMessageHandler = navigator.mozSetMessageHandler;
navigator.mozSetMessageHandler = MockNavigatormozSetMessageHandler;
MockNavigatormozSetMessageHandler.mSetup();
realMozAlarms = navigator.mozAlarms;
navigator.mozAlarms = MockMozAlarms;
realNavigatorOnLine = Object.getOwnPropertyDescriptor(navigator, 'onLine');
Object.defineProperty(navigator, 'onLine', {
fakeOnLine: true,
configurable: true,
get: function() { return this.fakeOnLine; },
set: function(status) { this.fakeOnLine = !!status; }
});
window.Config = {
api_url: 'https://find.firefox.com',
api_version: 'v0'
};
observerSpy = sinon.spy(MockNavigatorSettings, 'addObserver');
// We require findmydevice.js here and not above because
// we want to make sure all of our dependencies have already
// been loaded.
require('/js/findmydevice.js', function() {
// We stub navigator.mozId.watch above, so FindMyDevice.init()
// is effectively a no-op. We don't need to fully initialize FMD for
// these tests, so just register for the events we care about below.
FindMyDevice._observeSettings();
FindMyDevice._initMessageHandlers();
done();
});
});
suiteTeardown(function() {
observerSpy.restore();
delete window.Config;
navigator.mozId = realMozId;
navigator.mozSettings = realMozSettings;
MockNavigatorSettings.mTeardown();
navigator.mozSetMessageHandler = realMozSetMessageHandler;
MockNavigatormozSetMessageHandler.mTeardown();
navigator.mozAlarms = realMozAlarms;
if (realNavigatorOnLine) {
Object.defineProperty(navigator, 'onLine', realNavigatorOnLine);
}
});
setup(function(done) {
this.sinon.stub(FindMyDevice, '_reportDisabled');
this.sinon.stub(FindMyDevice, '_replyAndFetchCommands');
realCommands = window.Commands;
window.Commands = {
invokeCommand: this.sinon.stub(),
deviceHasPasscode: null
};
// XXX(ggp) force re-creation of the SettingsHelper objects
// used by FMD, since MockSettingsHelper invalidates all objects
// in its mTeardown.
FindMyDevice._initSettings(done);
navigator.onLine = true;
});
teardown(function() {
FindMyDevice._enabled = false;
FindMyDevice._registered = false;
FindMyDevice._loggedIn = false;
FindMyDevice._state = null;
window.Commands = realCommands;
});
function sendWakeUpMessage(reason) {
var port = {};
MockNavigatormozSetMessageHandler.mTrigger('connection',
{port: port, keyword: 'findmydevice-wakeup'});
port.onmessage({data: reason});
}
test('ensure retry counter is reset on enable', function() {
sendWakeUpMessage(IAC_API_WAKEUP_REASON_ENABLED_CHANGED);
MockSettingsHelper('findmydevice.retry-count').get(
function(val) {
assert.equal(val, 0, 'retry count should be 0');
});
});
test('retryCount is not incremented on error if registered', function() {
FindMyDevice._registered = true;
sendWakeUpMessage(IAC_API_WAKEUP_REASON_ENABLED_CHANGED);
this.sinon.stub(FindMyDevice, 'beginHighPriority');
this.sinon.stub(FindMyDevice, 'endHighPriority');
// simulate 3 failed requests
FindMyDevice._handleServerError({status:401});
FindMyDevice._handleServerError({status:401});
FindMyDevice._handleServerError({status:401});
MockSettingsHelper('findmydevice.retry-count').get(
function(val) {
assert.equal(val, 0, 'retry count should be 0');
});
});
test('retryCount is incremented on error when not registered', function() {
FindMyDevice._registered = false;
sendWakeUpMessage(IAC_API_WAKEUP_REASON_ENABLED_CHANGED);
this.sinon.stub(FindMyDevice, 'beginHighPriority');
this.sinon.stub(FindMyDevice, 'endHighPriority');
// simulate 3 failed requests
FindMyDevice._handleServerError({status:401});
FindMyDevice._handleServerError({status:401});
FindMyDevice._handleServerError({status:401});
MockSettingsHelper('findmydevice.retry-count').get(
function(val) {
assert.equal(val, 3, 'retry count should be 3');
});
});
test('retryCount is not incremented when offline', function() {
FindMyDevice._registered = false;
sendWakeUpMessage(IAC_API_WAKEUP_REASON_ENABLED_CHANGED);
this.sinon.stub(FindMyDevice, 'beginHighPriority');
this.sinon.stub(FindMyDevice, 'endHighPriority');
navigator.onLine = false;
// Simulate 3 failed requests
FindMyDevice._handleServerError({status: 401});
FindMyDevice._handleServerError({status: 401});
FindMyDevice._handleServerError({status: 401});
MockSettingsHelper('findmydevice.retry-count').get(
function(val) {
assert.equal(val, 0, 'retry count should be 0');
});
});
test('fields from coordinates are included in server response', function() {
FindMyDevice._registered = true;
FindMyDevice._enabled = true;
FindMyDevice._replyCallback('t', true, MockGeolocation.fakePosition);
sinon.assert.called(FindMyDevice._replyAndFetchCommands);
assert.deepEqual(FindMyDevice._reply.t, {
ok: true,
la: MockGeolocation.fakePosition.coords.latitude,
lo: MockGeolocation.fakePosition.coords.longitude,
acc: MockGeolocation.fakePosition.coords.accuracy,
ti: MockGeolocation.fakePosition.timestamp
});
});
test('error message is included in the server response', function() {
FindMyDevice._registered = true;
FindMyDevice._enabled = true;
var message = 'error message';
FindMyDevice._replyCallback('t', false, message);
sinon.assert.called(FindMyDevice._replyAndFetchCommands);
assert.equal(FindMyDevice._reply.t.error, message);
});
suite('settings observers', function() {
test('Observes findmydevice.registered', function() {
sinon.assert.calledWith(observerSpy, 'findmydevice.registered');
assert.isTrue('_registered' in FindMyDevice);
assert.equal('object', typeof FindMyDevice._registeredHelper);
assert.notEqual(null, FindMyDevice._registeredHelper);
});
test('Observes findmydevice.enabled', function() {
sinon.assert.calledWith(observerSpy, 'findmydevice.enabled');
assert.isTrue('_enabled' in FindMyDevice);
assert.equal('object', typeof FindMyDevice._enabledHelper);
assert.notEqual(null, FindMyDevice._enabledHelper);
});
test('Observes findmydevice.current-clientid', function() {
sinon.assert.calledWith(observerSpy, 'findmydevice.current-clientid');
assert.isTrue('_currentClientID' in FindMyDevice);
assert.equal('object', typeof FindMyDevice._currentClientIDHelper);
assert.notEqual(null, FindMyDevice._currentClientIDHelper);
});
test('Observes findmydevice.can-disable', function() {
sinon.assert.calledWith(observerSpy, 'findmydevice.can-disable');
assert.isTrue('_canDisable' in FindMyDevice);
assert.equal('object', typeof FindMyDevice._canDisableHelper);
assert.notEqual(null, FindMyDevice._canDisableHelper);
});
});
suite('settings handlers', function() {
var endHighPriorityStub;
suite('Changes to findmydevice.registered', function() {
var contactServerSpy, loadStateSpy, currentIdHelperSpy;
var _registered = {};
setup(function() {
contactServerSpy = this.sinon.spy(FindMyDevice, '_contactServer');
loadStateSpy = this.sinon.spy(FindMyDevice, '_loadState');
currentIdHelperSpy =
this.sinon.spy(FindMyDevice._currentClientIDHelper, 'set');
endHighPriorityStub = this.sinon.stub(FindMyDevice, 'endHighPriority');
});
suite('when not registered', function() {
setup(function() {
_registered = { settingValue: false};
});
test('contact server', function() {
FindMyDevice._onRegisteredChanged(_registered);
sinon.assert.calledOnce(contactServerSpy);
sinon.assert.notCalled(loadStateSpy);
sinon.assert.notCalled(currentIdHelperSpy);
});
test('end wakelock section', function() {
FindMyDevice._onRegisteredChanged(_registered);
sinon.assert.calledWith(endHighPriorityStub, 'clientLogic');
sinon.assert.notCalled(loadStateSpy);
sinon.assert.notCalled(currentIdHelperSpy);
});
});
suite('when registered', function() {
setup(function() {
_registered = { settingValue: true};
});
test('does load state', function() {
FindMyDevice._onRegisteredChanged(_registered);
sinon.assert.calledOnce(loadStateSpy);
});
test('does contact server', function() {
FindMyDevice._onRegisteredChanged(_registered);
sinon.assert.calledOnce(contactServerSpy);
});
test('does set empty current id', function() {
FindMyDevice._onRegisteredChanged(_registered);
sinon.assert.calledOnce(currentIdHelperSpy);
sinon.assert.calledWith(currentIdHelperSpy, '');
});
});
});
test('Changes to findmydevice.enabled', function() {
assert.isTrue(true, 'nothing to do');
});
suite('Changes to findmydevice.current-clientid', function() {
var _currentClientId = {};
var refreshClientStub;
setup(function() {
endHighPriorityStub = this.sinon.stub(FindMyDevice, 'endHighPriority');
refreshClientStub =
this.sinon.stub(FindMyDevice, '_refreshClientIDIfRegistered');
});
suite('logged and no clientid', function() {
var oldLogged;
setup(function() {
_currentClientId = { settingValue: ''};
oldLogged = FindMyDevice._loggedIn;
FindMyDevice._loggedIn = true;
});
teardown(function() {
FindMyDevice._loggedIn = oldLogged;
});
test('client id refresh', function() {
FindMyDevice._onClientIDChanged(_currentClientId);
sinon.assert.calledOnce(refreshClientStub);
sinon.assert.calledWith(refreshClientStub, false);
});
test('end of wakelock section', function() {
FindMyDevice._onClientIDChanged(_currentClientId);
sinon.assert.calledWith(endHighPriorityStub, 'clientLogic');
});
});
suite('not logged/has clientid and registered', function() {
var oldLogged, oldRegistered, oldState;
setup(function() {
_currentClientId = { settingValue: 'cid'};
oldLogged = FindMyDevice._loggedIn;
oldRegistered = FindMyDevice._registered;
oldState = FindMyDevice._state;
FindMyDevice._loggedIn = false;
FindMyDevice._registered = true;
FindMyDevice._state = { };
});
teardown(function() {
FindMyDevice._loggedIn = oldLogged;
FindMyDevice._registered = oldRegistered;
FindMyDevice._state = oldState;
});
suite('calls to canDisableHelper', function() {
var canDisableSpy;
setup(function() {
canDisableSpy =
this.sinon.spy(FindMyDevice._canDisableHelper, 'set');
});
suite('with everything true', function() {
setup(function() {
FindMyDevice._loggedIn = true;
FindMyDevice._state.clientid = 'cid';
});
test('proper call to canDisableHelper', function() {
FindMyDevice._onClientIDChanged(_currentClientId);
sinon.assert.calledOnce(canDisableSpy);
sinon.assert.calledWith(canDisableSpy, true);
});
});
suite('with loggedin false and matching clientid', function() {
setup(function() {
FindMyDevice._loggedIn = false;
FindMyDevice._state.clientid = 'cid';
});
test('proper call to canDisableHelper', function() {
FindMyDevice._onClientIDChanged(_currentClientId);
sinon.assert.calledOnce(canDisableSpy);
sinon.assert.calledWith(canDisableSpy, false);
});
});
suite('with loggedin false and non matching clientid', function() {
setup(function() {
FindMyDevice._loggedIn = false;
FindMyDevice._state.clientid = '';
});
test('proper call to canDisableHelper', function() {
FindMyDevice._onClientIDChanged(_currentClientId);
sinon.assert.calledOnce(canDisableSpy);
sinon.assert.calledWith(canDisableSpy, false);
});
});
suite('with loggedin true and non matching clientid', function() {
setup(function() {
FindMyDevice._loggedIn = true;
FindMyDevice._state.clientid = '';
});
test('proper call to canDisableHelper', function() {
FindMyDevice._onClientIDChanged(_currentClientId);
sinon.assert.calledOnce(canDisableSpy);
sinon.assert.calledWith(canDisableSpy, false);
});
});
});
test('keep wakelock section', function() {
FindMyDevice._onClientIDChanged(_currentClientId);
sinon.assert.notCalled(endHighPriorityStub);
});
});
suite('none of the above', function() {
var oldLogged, oldRegistered;
setup(function() {
_currentClientId = { settingValue: 'cid'};
oldLogged = FindMyDevice._loggedIn;
oldRegistered = FindMyDevice._registered;
FindMyDevice._loggedIn = false;
FindMyDevice._registered = false;
});
teardown(function() {
FindMyDevice._loggedIn = oldLogged;
FindMyDevice._registered = oldRegistered;
});
test('end of wakelock section', function() {
FindMyDevice._onClientIDChanged(_currentClientId);
sinon.assert.calledWith(endHighPriorityStub, 'clientLogic');
sinon.assert.notCalled(refreshClientStub);
});
});
});
suite('Changes to findmydevice.can-disable', function() {
var _canDisable = {};
var enabledSpy;
var oldDisableAttempt;
setup(function() {
endHighPriorityStub = this.sinon.stub(FindMyDevice, 'endHighPriority');
enabledSpy =
this.sinon.spy(FindMyDevice._enabledHelper, 'set');
oldDisableAttempt = FindMyDevice._disableAttempt;
});
teardown(function() {
FindMyDevice._disableAttempt = oldDisableAttempt;
});
suite('can-disabled changed to true', function() {
setup(function() {
_canDisable = { settingValue: true};
});
test('with disable attempt', function() {
FindMyDevice._disableAttempt = true;
FindMyDevice._onCanDisableChanged(_canDisable);
sinon.assert.calledWith(enabledSpy, false);
});
test('without disable attempt', function() {
FindMyDevice._disableAttempt = false;
FindMyDevice._onCanDisableChanged(_canDisable);
sinon.assert.notCalled(enabledSpy);
});
test('reset stuff', function() {
FindMyDevice._onCanDisableChanged(_canDisable);
assert.isFalse(FindMyDevice._disableAttempt,
'set disableAttempt to false');
sinon.assert.calledOnce(endHighPriorityStub, 'end wakelock section');
});
});
suite('can-disabled changed to false', function() {
setup(function() {
_canDisable = { settingValue: false};
});
test('with disable attempt', function() {
FindMyDevice._disableAttempt = true;
FindMyDevice._onCanDisableChanged(_canDisable);
sinon.assert.notCalled(enabledSpy);
});
test('without disable attempt', function() {
FindMyDevice._disableAttempt = false;
FindMyDevice._onCanDisableChanged(_canDisable);
sinon.assert.notCalled(enabledSpy);
});
test('reset stuff', function() {
FindMyDevice._onCanDisableChanged(_canDisable);
assert.isFalse(FindMyDevice._disableAttempt,
'set disableAttempt to false');
sinon.assert.calledOnce(endHighPriorityStub, 'end wakelock section');
});
});
});
});
suite('findmydevice.current-clientid behavior', function() {
setup(function() {
this.sinon.spy(FindMyDevice._currentClientIDHelper, 'set');
});
test('invalidate client id when logged in', function() {
sendWakeUpMessage(IAC_API_WAKEUP_REASON_LOGIN);
assert.isTrue(FindMyDevice._loggedIn, 'not logged in after login event');
sinon.assert.calledWith(FindMyDevice._currentClientIDHelper.set, '');
});
test('invalidate client id when logged out', function() {
sendWakeUpMessage(IAC_API_WAKEUP_REASON_LOGOUT);
assert.isFalse(FindMyDevice._loggedIn, 'logged in after logout event');
sinon.assert.calledWith(FindMyDevice._currentClientIDHelper.set, '');
});
test('refresh the current clientid after (re-)registering',
function() {
FindMyDevice._enabled = true;
FindMyDevice._registered = true;
this.sinon.stub(FindMyDevice, '_loadState', function(cb) { cb(); });
FindMyDevice._onRegisteredChanged({settingValue: true});
sinon.assert.calledWith(FindMyDevice._currentClientIDHelper.set, '');
});
});
suite('findmydevice.can-disable behavior', function() {
setup(function() {
this.sinon.spy(FindMyDevice._canDisableHelper, 'set');
});
test('set findmydevice.can-disable to false when logged out', function() {
FindMyDevice._registered = true;
FindMyDevice._loggedIn = false;
MockNavigatorSettings.mTriggerObservers('findmydevice.current-clientid',
{settingValue: ''});
sinon.assert.calledWith(FindMyDevice._canDisableHelper.set, false);
});
test('don\'t set findmydevice.can-disable on logout if not registered',
function() {
FindMyDevice._loggedIn = false;
FindMyDevice._registered = false;
MockNavigatorSettings.mTriggerObservers('findmydevice.current-clientid',
{settingValue: ''});
sinon.assert.notCalled(FindMyDevice._canDisableHelper.set);
});
test('allow disabling when clientid matches the state', function() {
FindMyDevice._loggedIn = true;
FindMyDevice._registered = true;
FindMyDevice._state = {clientid: 'clientid'};
MockNavigatorSettings.mTriggerObservers('findmydevice.current-clientid',
{settingValue: 'clientid'});
sinon.assert.calledWith(FindMyDevice._canDisableHelper.set, true);
});
test('disallow disabling when clientid doesn\'t match the state',
function() {
FindMyDevice._loggedIn = true;
FindMyDevice._registered = true;
FindMyDevice._state = {clientid: 'wrong clientid'};
MockNavigatorSettings.mTriggerObservers('findmydevice.current-clientid',
{settingValue: 'clientid'});
sinon.assert.calledWith(FindMyDevice._canDisableHelper.set, false);
});
});
test('request client id when invalidated', function() {
FindMyDevice._loggedIn = true;
FindMyDevice._registered = true;
FindMyDevice._state = {clientid: 'clientid'};
this.sinon.stub(FindMyDevice, 'beginHighPriority');
this.sinon.stub(FindMyDevice, 'endHighPriority');
this.sinon.spy(FindMyDevice, '_refreshClientIDIfRegistered');
MockNavigatorSettings.mTriggerObservers('findmydevice.current-clientid',
{settingValue: ''});
sinon.assert.calledWith(FindMyDevice._refreshClientIDIfRegistered, false);
sinon.assert.calledWith(navigator.mozId.request, {});
sinon.assert.calledOnce(FindMyDevice.beginHighPriority);
sinon.assert.calledWith(FindMyDevice.beginHighPriority, 'clientLogic');
sinon.assert.calledOnce(FindMyDevice.endHighPriority);
sinon.assert.calledWith(FindMyDevice.endHighPriority, 'clientLogic');
});
test('refresh authentication when attempting to disable', function() {
FindMyDevice._loggedIn = true;
FindMyDevice._registered = true;
this.sinon.spy(FindMyDevice, '_refreshClientIDIfRegistered');
sendWakeUpMessage(IAC_API_WAKEUP_REASON_TRY_DISABLE);
assert.isTrue(FindMyDevice._disableAttempt);
sinon.assert.calledWith(FindMyDevice._refreshClientIDIfRegistered, true);
assert.isTrue(
navigator.mozId.request.calledWithMatch(
{refreshAuthentication: 0}));
});
test('setting an alarm releases a wakelock', function() {
var clock = this.sinon.useFakeTimers();
this.sinon.stub(FindMyDevice, 'endHighPriority');
FindMyDevice._scheduleAlarm('ping');
clock.tick();
sinon.assert.calledWith(FindMyDevice.endHighPriority, 'clientLogic');
clock.restore();
});
test('contact the server on alarm', function() {
this.sinon.stub(FindMyDevice, '_contactServer');
this.sinon.stub(FindMyDevice, '_refreshClientIDIfRegistered');
MockNavigatormozSetMessageHandler.mTrigger('alarm');
sinon.assert.calledWith(FindMyDevice._refreshClientIDIfRegistered, false);
sinon.assert.called(FindMyDevice._contactServer);
});
test('report to the server when disabled', function() {
FindMyDevice._registered = true;
MockNavigatorSettings.mTriggerObservers('findmydevice.enabled',
{settingValue: false});
sendWakeUpMessage(IAC_API_WAKEUP_REASON_ENABLED_CHANGED);
sinon.assert.called(FindMyDevice._reportDisabled);
});
test('track and ring are cancelled on LOCKSCREEN_CLOSED, passcode set',
function() {
this.sinon.stub(Commands, 'deviceHasPasscode', function() {return true;});
sendWakeUpMessage(IAC_API_WAKEUP_REASON_LOCKSCREEN_CLOSED);
sinon.assert.calledTwice(Commands.invokeCommand);
sinon.assert.calledWith(Commands.invokeCommand, 'ring', [0]);
sinon.assert.calledWith(Commands.invokeCommand, 'track', [0]);
});
test('track and ring continue on LOCKSCREEN_CLOSED, no passcode set',
function() {
this.sinon.stub(Commands, 'deviceHasPasscode', function() {return false;});
sendWakeUpMessage(IAC_API_WAKEUP_REASON_LOCKSCREEN_CLOSED);
sinon.assert.notCalled(Commands.invokeCommand);
});
test('wakelocks are released on unregistered clientID change', function() {
FindMyDevice._registered = false;
this.sinon.stub(FindMyDevice, 'endHighPriority');
MockNavigatorSettings.mTriggerObservers('findmydevice.current-clientid',
{settingValue: ''});
sinon.assert.calledOnce(FindMyDevice.endHighPriority);
sinon.assert.calledWith(FindMyDevice.endHighPriority, 'clientLogic');
});
test('wakelocks are released when registering while already registering',
function() {
this.sinon.stub(FindMyDevice, 'endHighPriority');
FindMyDevice._registering = true;
FindMyDevice._register();
sinon.assert.calledOnce(FindMyDevice.endHighPriority);
sinon.assert.calledWith(FindMyDevice.endHighPriority, 'clientLogic');
});
suite('testing command flow', function() {
var serverResponse = {};
var commandSpy;
setup(function() {
FindMyDevice._enabled = true;
commandSpy = this.sinon.spy(FindMyDevice, '_processCommands');
});
suite('set alarm on server interaction', function() {
var alarmSpy;
setup(function() {
serverResponse = {t: {d: 60}};
alarmSpy = this.sinon.spy(FindMyDevice, '_scheduleAlarm');
});
suite('With FMD enabled', function() {
setup(function() {
FindMyDevice._handleServerResponse(serverResponse);
});
test('alarm is set on successful server response', function() {
sinon.assert.calledOnce(commandSpy);
sinon.assert.calledWith(commandSpy, serverResponse);
sinon.assert.calledOnce(Commands.invokeCommand);
sinon.assert.calledWith(Commands.invokeCommand, 'track');
sinon.assert.calledWith(alarmSpy, 'ping');
});
});
suite('With FMD disabled', function() {
setup(function() {
FindMyDevice._enabled = false;
FindMyDevice._handleServerResponse(serverResponse);
});
teardown(function() {
FindMyDevice._enabled = true;
});
test('alarm is set on server response even when disabled', function() {
sinon.assert.notCalled(commandSpy);
sinon.assert.notCalled(Commands.invokeCommand);
sinon.assert.calledWith(alarmSpy, 'ping');
});
});
});
suite('Processing of erase command', function() {
setup(function() {
serverResponse = {e: {}};
FindMyDevice._handleServerResponse(serverResponse);
});
test('erase command invoked', function() {
sinon.assert.calledOnce(commandSpy);
sinon.assert.calledWith(commandSpy, serverResponse);
sinon.assert.calledOnce(Commands.invokeCommand);
sinon.assert.calledWith(Commands.invokeCommand, 'erase');
});
});
suite('Processing of killswitch command', function() {
setup(function() {
serverResponse = {k: {}};
FindMyDevice._handleServerResponse(serverResponse);
});
test('killswitch command invoked', function() {
sinon.assert.calledOnce(commandSpy);
sinon.assert.calledWith(commandSpy, serverResponse);
sinon.assert.calledOnce(Commands.invokeCommand);
sinon.assert.calledWith(Commands.invokeCommand, 'killswitch');
});
});
suite('Processing of lock command', function() {
setup(function() {
serverResponse = {l: {m: 'message', c: '1234'}};
FindMyDevice._handleServerResponse(serverResponse);
});
test('lock command invoked', function() {
sinon.assert.calledOnce(commandSpy);
sinon.assert.calledWith(commandSpy, serverResponse);
sinon.assert.calledOnce(Commands.invokeCommand);
sinon.assert.calledWith(Commands.invokeCommand, 'lock');
var args = Commands.invokeCommand.firstCall.args[1];
assert.equal(args[0], 'message');
assert.equal(args[1], '1234');
});
});
suite('Processing of ring command', function() {
setup(function() {
serverResponse = {r: {d: 5}};
FindMyDevice._handleServerResponse(serverResponse);
});
test('ring command invoked', function() {
sinon.assert.calledOnce(commandSpy);
sinon.assert.calledWith(commandSpy, serverResponse);
sinon.assert.calledOnce(Commands.invokeCommand);
sinon.assert.calledWith(Commands.invokeCommand, 'ring');
var args = Commands.invokeCommand.firstCall.args[1];
assert.equal(args[0], 5);
});
});
suite('Processing of track command', function() {
setup(function() {
serverResponse = {t: {d: 7}};
FindMyDevice._handleServerResponse(serverResponse);
});
test('track command invoked', function() {
sinon.assert.calledOnce(commandSpy);
sinon.assert.calledWith(commandSpy, serverResponse);
sinon.assert.calledOnce(Commands.invokeCommand);
sinon.assert.calledWith(Commands.invokeCommand, 'track');
var args = Commands.invokeCommand.firstCall.args[1];
assert.equal(args[0], 7);
});
});
});
});

View file

@ -0,0 +1,3 @@
'use strict';
require('/shared/test/unit/mocks/mocks_helper.js');