Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
926 lines
30 KiB
JavaScript
926 lines
30 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
'use strict';
|
|
|
|
/* global asyncStorage */
|
|
/* global BaseModule */
|
|
/* global ERROR_INVALID_SYNC_ACCOUNT */
|
|
/* global ERROR_NO_KEY_FETCH_TOKEN */
|
|
/* global ERROR_REQUEST_SYNC_REGISTRATION */
|
|
/* global ERROR_SYNC_APP_GENERIC */
|
|
/* global ERROR_SYNC_APP_KILLED */
|
|
/* global ERROR_SYNC_REQUEST */
|
|
/* global ERROR_UNVERIFIED_ACCOUNT */
|
|
/* global FxAccountsClient */
|
|
/* global IACHandler */
|
|
/* global LazyLoader */
|
|
/* global Service */
|
|
/* global SyncErrors */
|
|
/* global SyncRecoverableErrors */
|
|
/* global Telemetry */
|
|
/* global uuid */
|
|
|
|
/**
|
|
* This module manages the lifecycle of Firefox Sync on Firefox OS.
|
|
* It connects the Settings application where the Firefox Sync UI is presented
|
|
* to the user with the Sync app that is responsible of doing the actual
|
|
* synchronization of the user's data.
|
|
*
|
|
* SyncManager reacts to changes done on the Settings Sync UI that triggers
|
|
* IAC requests or Settings API changes and translates these changes into
|
|
* actions. It makes use of the SyncStateMachine to control that these actions
|
|
* are possible on the current situation of Sync and communicates with the
|
|
* Sync application via IAC API messages when needed.
|
|
*
|
|
* The IAC based API that is exposed to the Settings app (or any other
|
|
* future consumer that requires to manage Sync) expects IAC messages with
|
|
* a single property 'name' that can have any of the following values:
|
|
* 'enable': requests to enable Sync.
|
|
* 'disable': requests to disable Sync.
|
|
* 'sync': requests a sync on demand. Although we have scheduled
|
|
* synchronizations, we allow on demand sync requests.
|
|
* 'getInfo': gets the current information about the state of Sync
|
|
* including the state (enabled, disabled, etc.), the
|
|
* last time a successful sync happened and the last error
|
|
* if there's any.
|
|
*
|
|
* It also triggers one message through the same IAC API with name
|
|
* 'onsyncchange' when something (state, error, lastSync) changes on Sync.
|
|
*
|
|
* SyncManager also communicates with the Sync app via IAC. Every time a
|
|
* synchronization is required (either because the requestSync timer fires or
|
|
* because the user requested a sync on demand), we wake the Sync app up and
|
|
* give it the required information to perform the synchronization that
|
|
* includes the URL of the Syncto server, a valid FxA assertion and its
|
|
* associated encryption keys and the list of collections to be synchronized.
|
|
*/
|
|
|
|
(function(exports) {
|
|
const FXA_EVENT = 'mozFxAccountsUnsolChromeEvent';
|
|
|
|
const SYNC_TASK = 'gaia::system::firefoxsync';
|
|
const SYNC_REQUEST_IAC_KEYWORD = 'gaia::sync::request';
|
|
const SYNC_MANAGEMENT_API_IAC_KEYWORD = 'gaia-sync-management';
|
|
|
|
const COLLECTIONS = ['history', 'passwords', 'bookmarks'];
|
|
|
|
// Keys of asyncStorage persisted data.
|
|
const SYNC_STATE = 'sync.state';
|
|
const SYNC_STATE_ERROR = 'sync.state.error';
|
|
const SYNC_LAST_TIME = 'sync.lastTime';
|
|
const SYNC_USER = 'sync.user';
|
|
|
|
var SyncManager = function() {};
|
|
|
|
SyncManager.STATES = [];
|
|
|
|
SyncManager.SERVICES = [];
|
|
|
|
SyncManager.SETTINGS = [
|
|
/** READ ONLY SETTINGS **/
|
|
|
|
// The user can choose the collections she wants to sync. These settings
|
|
// indicates the user choice per collection.
|
|
'sync.collections.history.enabled',
|
|
'sync.collections.passwords.enabled',
|
|
'sync.collections.bookmarks.enabled',
|
|
|
|
// Setting any of these two settings to true will make the synchronization
|
|
// of the collection readonly. That means that we will only be retrieving
|
|
// data from Firefox Sync and we won't be pushing any of the local
|
|
// modifications to the collections source.
|
|
'sync.collections.history.readonly',
|
|
'sync.collections.passwords.readonly',
|
|
'sync.collections.bookmarks.readonly',
|
|
|
|
// Limit the number of records fetched from the server in a single request.
|
|
// Once the sync engine supports pagination, the limit will be the page
|
|
// size.
|
|
'sync.collections.history.limit',
|
|
'sync.collections.passwords.limit',
|
|
'sync.collections.bookmarks.limit',
|
|
|
|
'sync.server.url',
|
|
'sync.scheduler.interval',
|
|
'sync.scheduler.wifionly',
|
|
|
|
'sync.fxa.audience'
|
|
];
|
|
|
|
SyncManager.EVENTS = [
|
|
/** IAC based sync management API **/
|
|
'iac-' + SYNC_MANAGEMENT_API_IAC_KEYWORD,
|
|
|
|
/** Sync State Machine events **/
|
|
'onsyncdisabled',
|
|
'onsyncdisabling',
|
|
'onsyncenabled',
|
|
'onsyncenabling',
|
|
'onsyncerrored',
|
|
'onsyncsyncing',
|
|
|
|
/** Sync app lifecycle **/
|
|
'appterminated'
|
|
];
|
|
|
|
SyncManager.SUB_MODULES = [
|
|
'SyncStateMachine'
|
|
];
|
|
|
|
BaseModule.create(SyncManager, {
|
|
name: 'SyncManager',
|
|
DEBUG: false,
|
|
EVENT_PREFIX: 'sync',
|
|
store: new Map(),
|
|
|
|
/** BaseModule life cycle **/
|
|
|
|
_start: function() {
|
|
return new Promise((resolve, reject) => {
|
|
LazyLoader.load('/shared/js/sync/errors.js', () => {
|
|
navigator.mozSetMessageHandler('request-sync', event => {
|
|
this.debug('Sync request');
|
|
if (event.task != SYNC_TASK) {
|
|
return;
|
|
}
|
|
Service.request('SyncStateMachine:sync');
|
|
});
|
|
|
|
this.initStore().then(() => {
|
|
this.debug('Sync.state setting initial value', this.state);
|
|
// SyncStateMachine starts by default with 'disabled', so we can
|
|
// request to enable sync or stay in the disabled state.
|
|
// We need to make sure that we didn't end up in an inconsistent
|
|
// state during the latest run (i.e. error, success, syncing,
|
|
// enabling, disabling).
|
|
switch(this.state) {
|
|
case 'enabled':
|
|
case 'success':
|
|
case 'syncing':
|
|
Service.request('SyncStateMachine:enable').then(resolve);
|
|
break;
|
|
case 'disabling':
|
|
case 'enabling':
|
|
this.updateState('disabled');
|
|
resolve();
|
|
break;
|
|
case 'errored':
|
|
if (this.error == ERROR_UNVERIFIED_ACCOUNT) {
|
|
// We try to enable Sync. This will check if the account is
|
|
// verified or not. If it's not verified, we go back to the
|
|
// errored state, otherwise, the user logs in.
|
|
Service.request('SyncStateMachine:enable').then(resolve);
|
|
break;
|
|
}
|
|
this.updateState('disabled');
|
|
resolve();
|
|
break;
|
|
case 'disabled':
|
|
resolve();
|
|
break;
|
|
default:
|
|
console.error('Unexpected sync.state value', this.state);
|
|
reject();
|
|
break;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
_stop: function() {},
|
|
|
|
/** Event handlers **/
|
|
|
|
// We expose a very simple IAC based API to allow the Settings app to
|
|
// manage Sync.
|
|
//
|
|
// We expect IAC messages to contain a property named 'name' with any
|
|
// of the following values corresponding to the requests that we are
|
|
// able to handle:
|
|
// - enable
|
|
// - disable
|
|
// - sync
|
|
// - getInfo
|
|
//
|
|
// SyncStateMachine is responsible of checking if the request is valid in
|
|
// the current situation and progressing it accordingly by publishing the
|
|
// corresponding events.
|
|
'_handle_iac-gaia-sync-management': function(event) {
|
|
if (!event || !event.detail) {
|
|
console.error('Wrong IAC Sync event');
|
|
return;
|
|
}
|
|
|
|
var request = event.detail;
|
|
switch(request.name) {
|
|
case 'enable':
|
|
case 'disable':
|
|
case 'sync':
|
|
try {
|
|
Service.request('SyncStateMachine:' + request.name).then(() => {
|
|
return LazyLoader.load('/shared/js/sync/telemetry.js');
|
|
}).then(() => {
|
|
Telemetry.logUserAction(request.name);
|
|
}).catch(e => {
|
|
console.error(e);
|
|
});
|
|
} catch(e) {
|
|
console.error(e);
|
|
}
|
|
break;
|
|
case 'getInfo':
|
|
if (!request.id) {
|
|
console.warn('IAC request ignored. Missing id');
|
|
return;
|
|
}
|
|
this.getAccount().then(() => {
|
|
this.managementPortMessage({
|
|
id: request.id,
|
|
state: this.state,
|
|
error: this.error,
|
|
lastSync: this.lastSync,
|
|
user: this.user
|
|
});
|
|
});
|
|
break;
|
|
default:
|
|
console.warn('Unknown IAC Sync request', request.name);
|
|
break;
|
|
}
|
|
},
|
|
|
|
/** SyncStateMachine event handlers **/
|
|
|
|
_handle_onsyncdisabled: function() {
|
|
this.debug('onsyncdisabled observed');
|
|
this.updateState();
|
|
this.user = null;
|
|
this.lastSync = null;
|
|
this.error = null;
|
|
this.cleanup();
|
|
},
|
|
|
|
_handle_onsyncdisabling: function() {
|
|
this.debug('onsyncdisabling observed');
|
|
this.updateState();
|
|
|
|
// Go back to default settings, so new users won't inherit old users'
|
|
// preferences.
|
|
this.saveUserSettings().then(() => {
|
|
return this.restoreDefaultSettings();
|
|
}).then(() => {
|
|
Service.request('SyncStateMachine:success');
|
|
});
|
|
},
|
|
|
|
_handle_onsyncenabled: function(event) {
|
|
this.debug('onsyncenabled observed');
|
|
this.updateState();
|
|
|
|
// If we got to this point with no user, something went wrong, so we
|
|
// need to disable Sync.
|
|
if (!this.user) {
|
|
this.debug('No user');
|
|
Service.request('SyncStateMachine:disable');
|
|
return;
|
|
}
|
|
|
|
this.registerSyncRequest().then(() => {
|
|
this.debug('Sync request registered');
|
|
var from = event.detail && event.detail.from;
|
|
// After login in with a new account or rebooting the device,
|
|
// we trigger a sync request.
|
|
if (from === 'enabling') {
|
|
Service.request('SyncStateMachine:sync');
|
|
}
|
|
}).catch(error => {
|
|
console.error('Could not register sync request', error);
|
|
Service.request('SyncStateMachine:error',
|
|
ERROR_REQUEST_SYNC_REGISTRATION);
|
|
});
|
|
},
|
|
|
|
_handle_onsyncenabling: function() {
|
|
this.debug('onsyncenabling observed');
|
|
this.updateState();
|
|
|
|
// Because we need to bind this to the event handler, we need to save
|
|
// a reference to it so we can properly remove it later.
|
|
this.fxaEventHandler = this._handle_fxaEvent.bind(this);
|
|
window.addEventListener(FXA_EVENT, this.fxaEventHandler);
|
|
|
|
// We need to verify that we have everything that we need to start
|
|
// synchronizing data. This is: a valid FxA, the ability to obtain
|
|
// a valid assertion and encryption keys and the remote crypto/keys
|
|
// record.
|
|
var args = [];
|
|
this.updateStateDeferred.then(() => {
|
|
return this.getAssertion();
|
|
}).then(assertion =>{
|
|
args.push(assertion);
|
|
return this.getKeys();
|
|
}).then(keys => {
|
|
args.push(keys);
|
|
return this.getAccount();
|
|
}).then(() => {
|
|
// We request a sync without any collection. This should fetch the
|
|
// crypto/keys object. If it is available, we will successfully
|
|
// continue the enabling process. If it is not available, that probably
|
|
// means that this FxA has never been used before with Sync.
|
|
// In that case, until we are able to create new Sync user, we need
|
|
// to disable Sync and let the user know about this situation.
|
|
return this.trySync.apply(this, args);
|
|
}).then(() => {
|
|
// We save default settings so we can revert user changes to the
|
|
// default values when the user logs out from Sync. So new users don't
|
|
// inherit old users' preferences.
|
|
return this.saveDefaultSettings();
|
|
}).then(() => {
|
|
return this.restoreUserSettings();
|
|
}).then(() => {
|
|
Service.request('SyncStateMachine:success');
|
|
}).catch(error => {
|
|
// XXX Bug 1200284 - Normalize all Firefox Accounts error reporting.
|
|
error = error.message || error.name || error.error || error;
|
|
console.error('Could not enable sync', error);
|
|
|
|
if (error == 'UNVERIFIED_ACCOUNT') {
|
|
this.getAccount().then(() => {
|
|
Service.request('SyncStateMachine:error', ERROR_UNVERIFIED_ACCOUNT);
|
|
});
|
|
return;
|
|
}
|
|
|
|
error = SyncErrors[error] || error;
|
|
|
|
Service.request('SyncStateMachine:error', error);
|
|
});
|
|
},
|
|
|
|
_handle_onsyncerrored: function(event) {
|
|
this.debug('onsyncerrored observed');
|
|
|
|
var error = event.detail && Array.isArray(event.detail.args) ?
|
|
event.detail.args[0] : null;
|
|
if (!error) {
|
|
return;
|
|
}
|
|
|
|
this.error = error;
|
|
|
|
// We don't update the state until we set the error.
|
|
this.updateState();
|
|
|
|
var from = event.detail && event.detail.from;
|
|
|
|
// There are some cases where we want to show to the user
|
|
// the enabled state but warn her about the invalid state of her
|
|
// account. So we stay in the errored state.
|
|
if ([ERROR_UNVERIFIED_ACCOUNT].indexOf(error) > -1) {
|
|
return;
|
|
}
|
|
|
|
// If the error is recoverable and we are coming from the syncing
|
|
// state, we go back to the enabled state, otherwise, we disable Sync.
|
|
if ((SyncRecoverableErrors.indexOf(error) > -1 && from === 'syncing') ||
|
|
// XXX ERROR_INVALID_SYNC_ACCOUNT is an unrecoverable error, but we
|
|
// temporarily treat it in a different way depending on the device.
|
|
// On TVs, we show a dialog asking the user to go to Desktop or
|
|
// Android to create an account and we move to the disabled state.
|
|
// On phones, we move to the enabled state, but we show a warning to
|
|
// the user about her account being empty.
|
|
error == ERROR_INVALID_SYNC_ACCOUNT) {
|
|
Service.request('SyncStateMachine:enable');
|
|
return;
|
|
}
|
|
|
|
// If the current FxA login does not contain the token to fetch sync keys
|
|
// we need to log the user out. Unfortunately the FxA auth server does not
|
|
// allow to obtain this token outside of the initial sign in flow.
|
|
if (error == ERROR_NO_KEY_FETCH_TOKEN) {
|
|
// No need to disable Sync here as we will be doing it as soon as the
|
|
// logout is done.
|
|
FxAccountsClient.logout();
|
|
return;
|
|
}
|
|
|
|
this.debug('Unrecoverable error');
|
|
Service.request('SyncStateMachine:disable');
|
|
},
|
|
|
|
_handle_onsyncsyncing: function() {
|
|
this.debug('onsyncsyncing observed');
|
|
this.updateState();
|
|
|
|
// In case that we are syncing on user demand, we unregister the
|
|
// sync request.
|
|
// It's harmless to unregister an expired request, so it's also ok
|
|
// to do it for a periodic sync.
|
|
this.unregisterSyncRequest();
|
|
|
|
if (!navigator.onLine) {
|
|
Service.request('SyncStateMachine:success');
|
|
return;
|
|
}
|
|
|
|
var collections = {};
|
|
COLLECTIONS.forEach(name => {
|
|
if (this._settings['sync.collections.' + name + '.enabled']) {
|
|
collections[name] = {
|
|
readonly: this._settings['sync.collections.' + name + '.readonly'],
|
|
limit: this._settings['sync.collections.' + name + '.limit']
|
|
};
|
|
}
|
|
});
|
|
|
|
if (!Object.keys(collections).length) {
|
|
Service.request('SyncStateMachine:success');
|
|
return;
|
|
}
|
|
|
|
var args = [];
|
|
this.getAssertion().then(assertion => {
|
|
args.push(assertion);
|
|
return this.getKeys();
|
|
}).then(keys => {
|
|
args.push(keys);
|
|
args.push(collections);
|
|
this.doSync.apply(this, args);
|
|
}).catch(error => {
|
|
console.error('Sync error', error);
|
|
Service.request('SyncStateMachine:error', ERROR_SYNC_REQUEST);
|
|
});
|
|
},
|
|
|
|
_handle_fxaEvent: function(event) {
|
|
// We don't care about FxA events if we are already disabling.
|
|
if (this.state === 'disabling') {
|
|
return;
|
|
}
|
|
|
|
if (!event || !event.detail) {
|
|
console.error('Wrong event');
|
|
return;
|
|
}
|
|
|
|
switch (event.detail.eventName) {
|
|
case 'onlogout':
|
|
// Logging out from Firefox Accounts should disable Sync.
|
|
Service.request('SyncStateMachine:disable');
|
|
break;
|
|
case 'onverified':
|
|
// Once the user verified her account, we can continue with the
|
|
// enabling procedure.
|
|
if (this.state == 'errored' &&
|
|
this.error == ERROR_UNVERIFIED_ACCOUNT) {
|
|
this.getAccount().then(() => {
|
|
if (this.user) {
|
|
Service.request('SyncStateMachine:enable');
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
_handle_appterminated(event) {
|
|
// If the Sync app is closed, we need to release the reference to
|
|
// its IAC port, so we can get a new connection on the next sync
|
|
// request. Unfortunately, the IAC API doesn't take care of
|
|
// notifying when a port is dead. So we need to do it manually.
|
|
if (!this._port) {
|
|
return;
|
|
}
|
|
this.isSyncApp(event.detail ? event.detail.origin : null).then(() => {
|
|
this._port = null;
|
|
});
|
|
},
|
|
|
|
onportclose() {
|
|
// The synchronizer app can be killed while it's handling a sync
|
|
// request. In that case, we need to record an error and notify
|
|
// the state machine about it. Otherwise we could end up on a
|
|
// permanent 'syncing' state.
|
|
if (this.state !== 'syncing') {
|
|
return;
|
|
}
|
|
this.debug('Synchronizer closed before completing the sync request');
|
|
Service.request('SyncStateMachine:error', ERROR_SYNC_APP_KILLED);
|
|
},
|
|
|
|
/** Helpers **/
|
|
|
|
initStore: function() {
|
|
var promises = [];
|
|
[SYNC_STATE,
|
|
SYNC_STATE_ERROR,
|
|
SYNC_LAST_TIME,
|
|
SYNC_USER].forEach(key => {
|
|
var promise = new Promise(resolve => {
|
|
asyncStorage.getItem(key, value => {
|
|
this.debug(key + ': ' + value);
|
|
this.store.set(key, value);
|
|
resolve();
|
|
});
|
|
});
|
|
promises.push(promise);
|
|
});
|
|
return Promise.all(promises);
|
|
},
|
|
|
|
updateState: function(state) {
|
|
this.state = state || Service.query('SyncStateMachine.state');
|
|
},
|
|
|
|
registerSyncRequest: function() {
|
|
return navigator.sync.registrations().then(registrations => {
|
|
var existingReg;
|
|
registrations.some(reg => {
|
|
if (reg.task === SYNC_TASK) {
|
|
return !!(existingReg = reg);
|
|
}
|
|
});
|
|
|
|
if (existingReg) {
|
|
this.debug('Sync request already exists');
|
|
return Promise.resolve();
|
|
}
|
|
|
|
this.debug('Registering sync request', location.href);
|
|
|
|
// XXX We need to default to the settings value (3600 and false)
|
|
// because of
|
|
// Bug 1206004 - BaseModule._settings is not be available during
|
|
// BaseModule._start
|
|
var minInterval = this.DEBUG ? 120
|
|
: this._settings['sync.scheduler.interval'] || 3600;
|
|
var wifiOnly = this._settings['sync.scheduler.wifionly'] || false;
|
|
|
|
return navigator.sync.register(SYNC_TASK, {
|
|
minInterval: minInterval, // minInterval is in seconds.
|
|
oneShot: true,
|
|
wifiOnly: wifiOnly,
|
|
data: null,
|
|
wakeUpPage: location.href
|
|
});
|
|
});
|
|
},
|
|
|
|
unregisterSyncRequest: function() {
|
|
navigator.sync.unregister(SYNC_TASK).catch(e => {
|
|
// A failure unregistering the sync task might be expected
|
|
// depending on the state we are (i.e. enabling). In any case,
|
|
// we show a warning, so a bug can be identified or discarded.
|
|
console.warn('Could not unregister sync task', e,
|
|
Service.query('SyncStateMachine.state'));
|
|
});
|
|
},
|
|
|
|
getKeys: function() {
|
|
return new Promise((resolve, reject) => {
|
|
LazyLoader.load('js/fx_accounts_client.js', () => {
|
|
FxAccountsClient.getKeys(resolve, reject);
|
|
});
|
|
});
|
|
},
|
|
|
|
getAssertion: function() {
|
|
return new Promise((resolve, reject) => {
|
|
LazyLoader.load('js/fx_accounts_client.js', () => {
|
|
FxAccountsClient.getAssertion({
|
|
audience: this._settings['sync.fxa.audience']
|
|
}, resolve, reject);
|
|
});
|
|
});
|
|
},
|
|
|
|
getAccount: function() {
|
|
return new Promise((resolve, reject) => {
|
|
LazyLoader.load('js/fx_accounts_client.js', () => {
|
|
FxAccountsClient.getAccount(account => {
|
|
this.user = account ? account.email : null;
|
|
resolve(account);
|
|
}, reject);
|
|
});
|
|
});
|
|
},
|
|
|
|
managementPortMessage: function(message) {
|
|
var port = IACHandler.getPort(SYNC_MANAGEMENT_API_IAC_KEYWORD, this);
|
|
if (!port) {
|
|
this.debug('No management API consumers');
|
|
// We may not have any consumers yet. So in that case
|
|
// we simply bail out here.
|
|
return;
|
|
}
|
|
port.postMessage(message);
|
|
},
|
|
|
|
notifyStateChange: function() {
|
|
this.managementPortMessage({
|
|
name: 'onsyncchange',
|
|
state: this.state,
|
|
error: this.error,
|
|
lastSync: this.lastSync,
|
|
user: this.user
|
|
});
|
|
},
|
|
|
|
connect: function() {
|
|
if (this._port) {
|
|
return Promise.resolve(this._port);
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
navigator.mozApps.getSelf().onsuccess = event => {
|
|
var app = event.target.result;
|
|
app.connect(SYNC_REQUEST_IAC_KEYWORD, {
|
|
// For now we only allow one synchronizer app.
|
|
pageURLs: 'app://sync.gaiamobile.org'
|
|
}).then(ports => {
|
|
if (!ports || !ports.length) {
|
|
return reject();
|
|
}
|
|
this._port = ports[0];
|
|
this._port.onclose = this.onportclose.bind(this);
|
|
resolve(this._port);
|
|
}).catch(error => {
|
|
console.error(error);
|
|
reject(error);
|
|
});
|
|
};
|
|
});
|
|
},
|
|
|
|
iacRequest: function(request) {
|
|
return new Promise((resolve, reject) => {
|
|
this.connect().then(port => {
|
|
LazyLoader.load('/shared/js/uuid.js', () => {
|
|
var id = uuid();
|
|
request.id = id;
|
|
port.postMessage(request);
|
|
port.onmessage = event => {
|
|
var message = event.data;
|
|
if (!message || (message.id != id)) {
|
|
return;
|
|
}
|
|
resolve(message);
|
|
};
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
doSync: function(assertion, keys, collections) {
|
|
this.debug('Syncing with', JSON.stringify(collections));
|
|
this.iacRequest({
|
|
name: 'sync',
|
|
URL: this._settings['sync.server.url'],
|
|
assertion: assertion,
|
|
keys: keys,
|
|
collections: collections
|
|
}).then(result => {
|
|
if (result.error) {
|
|
var error = result.error;
|
|
error = error.message ? error.message : error;
|
|
console.error('Error while trying to sync', error);
|
|
|
|
error = SyncErrors[error] || ERROR_SYNC_APP_GENERIC;
|
|
|
|
Service.request('SyncStateMachine:error', error);
|
|
return;
|
|
}
|
|
|
|
// If we made a successfull synchronization we are good and so
|
|
// we don't care about past errors anymore.
|
|
this.error = null;
|
|
|
|
this.debug('Sync succeded');
|
|
this.lastSync = Date.now();
|
|
|
|
Service.request('SyncStateMachine:success');
|
|
});
|
|
},
|
|
|
|
trySync: function(assertion, keys) {
|
|
return this.iacRequest({
|
|
name: 'sync',
|
|
URL: this._settings['sync.server.url'],
|
|
assertion: assertion,
|
|
keys: keys,
|
|
collections: {}
|
|
}).then(result => {
|
|
if (result && result.error) {
|
|
console.error('Error trying sync', result.error.message);
|
|
throw result.error.message;
|
|
}
|
|
});
|
|
},
|
|
|
|
cancelSync: function() {
|
|
this.iacRequest({
|
|
name: 'cancel'
|
|
});
|
|
},
|
|
|
|
cleanup: function() {
|
|
this.unregisterSyncRequest();
|
|
window.removeEventListener(FXA_EVENT, this.fxaEventHandler);
|
|
// If we disabled Sync while a sync request was in progress, we need to
|
|
// cancel the request.
|
|
if (this.state === 'syncing') {
|
|
this.cancelSync();
|
|
}
|
|
},
|
|
|
|
isSyncApp: function(origin) {
|
|
if (!origin) {
|
|
return Promise.reject();
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
navigator.mozApps.getSelf().onsuccess = event => {
|
|
var app = event.target.result;
|
|
app.getConnections().then(connections => {
|
|
connections.forEach(connection => {
|
|
if (connection.keyword != SYNC_REQUEST_IAC_KEYWORD ||
|
|
connection.subscriber.indexOf(origin) < 0) {
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
reject();
|
|
});
|
|
};
|
|
});
|
|
},
|
|
|
|
updateStatePreference: function() {
|
|
// Changing the value of this setting changes the value of
|
|
// the 'services.sync.enabled' preference as well.
|
|
// We don't retrieve the Sync keys unless this pref is set to true.
|
|
const CHROME_EVENT = 'mozPrefChromeEvent';
|
|
return new Promise((resolve, reject) => {
|
|
var onprefchange = event => {
|
|
this.debug('Preference change', event.detail);
|
|
if (event.detail.prefName !== 'services.sync.enabled') {
|
|
return;
|
|
}
|
|
window.removeEventListener(CHROME_EVENT, onprefchange);
|
|
resolve();
|
|
};
|
|
window.addEventListener(CHROME_EVENT, onprefchange);
|
|
navigator.mozSettings.createLock().set({
|
|
'services.sync.enabled': this.state !== 'disabled'
|
|
}).onerror = reject;
|
|
});
|
|
},
|
|
|
|
saveSettings(prefix) {
|
|
var saveSetting = setting => {
|
|
return new Promise(resolve => {
|
|
asyncStorage.getItem(prefix + setting, value => {
|
|
if (value !== null) {
|
|
this.debug('Setting value already saved', setting);
|
|
resolve();
|
|
return;
|
|
}
|
|
this.debug('Saving setting', prefix + setting,
|
|
this._settings[setting]);
|
|
asyncStorage.setItem(prefix + setting, this._settings[setting],
|
|
resolve);
|
|
});
|
|
});
|
|
};
|
|
|
|
var promises = [];
|
|
SyncManager.SETTINGS.forEach(setting => {
|
|
promises.push(saveSetting(setting));
|
|
});
|
|
|
|
return Promise.all(promises);
|
|
},
|
|
|
|
saveDefaultSettings() {
|
|
this.debug('Saving default settings');
|
|
return this.saveSettings('');
|
|
},
|
|
|
|
saveUserSettings() {
|
|
this.debug('Saving user settings');
|
|
return this.saveSettings(this.user + '.');
|
|
},
|
|
|
|
restoreSettings(prefix) {
|
|
var revertSetting = setting => {
|
|
return new Promise((resolve, reject) => {
|
|
asyncStorage.getItem(prefix + setting, value => {
|
|
this.debug('Restoring setting ' + prefix + setting, value);
|
|
if (value === null) {
|
|
resolve();
|
|
return;
|
|
}
|
|
// We need to remove the setting from asyncStorage to allow
|
|
// default setting changes within OTA updates.
|
|
asyncStorage.removeItem(prefix + setting, () => {
|
|
var settingObject = {};
|
|
settingObject[setting] = value;
|
|
var req = navigator.mozSettings.createLock().set(settingObject);
|
|
req.onerror = error => {
|
|
console.error(error);
|
|
reject();
|
|
};
|
|
req.onsuccess = resolve;
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
var promises = [];
|
|
SyncManager.SETTINGS.forEach(setting => {
|
|
promises.push(revertSetting(setting));
|
|
});
|
|
|
|
return Promise.all(promises);
|
|
},
|
|
|
|
restoreDefaultSettings() {
|
|
this.debug('Restoring default settings');
|
|
return this.restoreSettings('');
|
|
},
|
|
|
|
restoreUserSettings() {
|
|
this.debug('Restoring user settings');
|
|
return this.restoreSettings(this.user + '.');
|
|
}
|
|
}, {
|
|
state: {
|
|
set: function(state) {
|
|
state = state || Service.query('SyncStateMachine.state');
|
|
this.debug('Setting state', state);
|
|
this.store.set(SYNC_STATE, state);
|
|
this.updateStateDeferred = new Promise(resolve => {
|
|
asyncStorage.setItem(SYNC_STATE, state, () => {
|
|
if (state === 'disabled' || state === 'enabled' ||
|
|
state === 'enabling') {
|
|
this.updateStatePreference().then(() => {
|
|
this.notifyStateChange();
|
|
resolve();
|
|
});
|
|
return;
|
|
}
|
|
this.notifyStateChange();
|
|
});
|
|
});
|
|
},
|
|
get: function() {
|
|
var state = this.store.get(SYNC_STATE);
|
|
return state ? state : 'disabled';
|
|
}
|
|
},
|
|
|
|
error: {
|
|
set: function(error) {
|
|
this.store.set(SYNC_STATE_ERROR, error);
|
|
asyncStorage.setItem(SYNC_STATE_ERROR, error);
|
|
},
|
|
get: function() {
|
|
return this.store.get(SYNC_STATE_ERROR);
|
|
}
|
|
},
|
|
|
|
lastSync: {
|
|
set: function(now) {
|
|
var lastSync = now;
|
|
this.store.set(SYNC_LAST_TIME, lastSync);
|
|
asyncStorage.setItem(SYNC_LAST_TIME, lastSync);
|
|
},
|
|
get: function() {
|
|
return this.store.get(SYNC_LAST_TIME);
|
|
}
|
|
},
|
|
|
|
user: {
|
|
set: function(user) {
|
|
// We need to check if there's a user logged in during the enabled
|
|
// state handler, so we cannot wait for the asyncStorage write.
|
|
this.store.set(SYNC_USER, user);
|
|
asyncStorage.setItem(SYNC_USER, user, () => {}, () => {
|
|
// This should never happen, but if something goes wrong storing the
|
|
// user we delete the user from memory and disable Sync to avoid
|
|
// inconsistent states.
|
|
this.store.delete(SYNC_USER);
|
|
Service.request('SyncStateMachine:disable');
|
|
});
|
|
},
|
|
get: function() {
|
|
return this.store.get(SYNC_USER);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Exported only for testing purposes.
|
|
exports.SyncManager = SyncManager;
|
|
}(window));
|