VulpesOS/gaia/apps/findmydevice/js/findmydevice.js
OverSu b38dea0597 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.
2026-09-30 23:13:03 +02:00

665 lines
20 KiB
JavaScript
Executable file

/* 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();