import { ILogger } from '@lenovo-software/lsa-clients-common'; import { SyncStorageDefaultInterval, SyncStorageMaxErrorsBeforeThrottle, SyncStorageThrottleBackOffAmount, SyncStorageThrottleMax } from '../common/defs'; import { Logger } from '../common/logger'; function desensitize(items: any): string { let objCopy = JSON.parse(JSON.stringify(items)); if (objCopy.hasOwnProperty('privateJWK')) { objCopy.privateJWK = '****'; } return JSON.stringify(objCopy); } class NVStorageWriter { private periodicWriterHandle: any; private writeFrequency: number; private origWriteFrequency: number; private hasChange: boolean; private container: any; private lastErrorCount: number; private cycleRunning: boolean; private logger: Logger; constructor(frequency: number) { this.periodicWriterHandle = null; this.writeFrequency = frequency || SyncStorageDefaultInterval; this.origWriteFrequency = this.writeFrequency; this.hasChange = false; this.container = {}; this.lastErrorCount = 0; this.cycleRunning = false; this.logger = Logger.getInstance(); } public hasUnsavedData() { return this.hasChange; } private isCycleRunning() { return this.cycleRunning; } public stopCycle() { this.cycleRunning = false; } public startPrefsWriter() { this.cycleRunning = true; this.stopPrefsWriter(); // We're using setTimeout() rather than setInterval() because it's possible that we might be waiting // for the sync storage API to complete, and end up running it again because the interval // is too short. An unlikely scenario, but using setTimeout() guarantees that won't happen. this.periodicWriterHandle = setTimeout(this.writePrefsInterval.bind(this), this.writeFrequency); } private stopPrefsWriter() { if (this.isPrefsWriterRunning()) { clearTimeout(this.periodicWriterHandle); } this.periodicWriterHandle = null; } public isPrefsWriterRunning() { return this.periodicWriterHandle !== null; } private writePrefsInterval() { this.periodicWriterHandle = null; if (!this.hasUnsavedData()) { this.startPrefsWriter(); return; } this.logger.logMessage('NVStorageWriter.writePrefsInterval(): Beginning sync...'); this.hasChange = false; chrome.storage.sync.set({ prefs: this.container }, () => { this.logger.logMessage('NVStorageWriter.writePrefsInterval(): Sync complete.'); if (!chrome.runtime.lastError) { this.logger.logMessage('*~*~*~ Saved: ' + JSON.stringify(desensitize(this.container))); this.lastErrorCount = 0; this.writeFrequency = this.origWriteFrequency; } else { this.hasChange = true; this.logger.logError( 'NVStorageWriter.writePrefsInterval(): Error writing to sync storage: ' + JSON.stringify(chrome.runtime.lastError) ); this.lastErrorCount++; if (this.lastErrorCount >= SyncStorageMaxErrorsBeforeThrottle) { if (this.origWriteFrequency === this.writeFrequency) { this.writeFrequency = SyncStorageThrottleBackOffAmount; } else { this.writeFrequency += SyncStorageThrottleBackOffAmount; } if (this.writeFrequency > SyncStorageThrottleMax) { this.writeFrequency = SyncStorageThrottleMax; } this.logger.logWarning( 'NVStorageWriter.writePrefsInterval(): Errors exceed maximum. Throttling write interval to ' + this.writeFrequency ); } } this.startPrefsWriter(); }); } writeRequest(prefsObj: any) { if (prefsObj) { this.container = prefsObj; this.logger.logMessage('NVStorageWriter.writeRequest(): ' + JSON.stringify(desensitize(this.container))); } this.hasChange = true; if (!this.isCycleRunning()) { this.logger.logMessage('NVStorageWriter.writeRequest(): Write interval not started. Starting it up...'); this.startPrefsWriter(); } } } export default class Preferences { private prefsContainer: any; private static singletonPrefs: Preferences; private _prefsLoaded = false; private writerInterval = 1000; private logger: ILogger; private nvStorageWriter = new NVStorageWriter(this.writerInterval); static ProvisioningData_Key = 'provisioningData'; static ProvisioningParams_Key = 'provisioningParams'; static Token_Key = 'token'; static PrivateKey_Key = 'privateJWK'; static PublicKey_Key = 'publicPEM'; static DisplayName_Key = 'displayName'; constructor() { this.prefsContainer = {}; this.logger = Logger.getInstance(); } //must be a singleton otherwise caching will get out of sync public static getInstance() { if (!this.singletonPrefs) { this.singletonPrefs = new Preferences(); } return this.singletonPrefs; } private hasUnsavedData(): boolean { return this.nvStorageWriter.hasUnsavedData(); } public async load(): Promise { return new Promise((resolve, reject) => { chrome.storage.sync.get('prefs', (items) => { if (chrome.runtime.lastError) { this.logger.logError('Unable to retrieve preferences: ' + chrome.runtime.lastError); reject(); return; } else this._prefsLoaded = true; if (items['prefs']) this.prefsContainer = items['prefs']; // TODO: Event subscriber: PubSub.publish("onPrefsStorageRetrieved", self.prefsContainer); this.logger.logMessage('*~*~*~ Retrieved: ' + JSON.stringify(desensitize(this.prefsContainer))); resolve(); }); }); } //a useful api for devs to clear out the preferences during testing public clearPrefs() { this.prefsContainer = {}; this.writePrefs(); } private prefsLoaded() { return this._prefsLoaded; } private writePrefs() { this.nvStorageWriter.writeRequest(this.prefsContainer); // TODO: Event subscriber: PubSub.publish("onPrefsUpdated", this.prefsContainer); } private saveMultipleSettings(obj: any) { if (typeof obj === 'object') { this.prefsContainer = Object.assign(this.prefsContainer, obj); // SHALLOW COPY ONLY! Don't pass in complex objects. this.writePrefs(); } else { this.logger.logError('preferences.saveMultipleSettings(): Must use object.'); } } public saveSetting(objName: string, objVal: any) { this.prefsContainer[objName] = objVal; this.writePrefs(); } private removeSettingFromObj(obj: any, objNames: string | Array) { let changed = false; if (typeof objNames === 'string') objNames = Array(objNames); for (let i = 0; i < objNames.length; i++) { let element = objNames[i]; if (obj.hasOwnProperty(element)) { this.logger.logMessage('*~*~*~ Removing: ' + element); changed = true; delete obj[element]; } else if (element.includes('.')) { let newObj = element.substring(0, element.indexOf('.')); let newElement = element.substring(element.indexOf('.') + 1); if (obj.hasOwnProperty(newObj)) { this.logger.logMessage('*~*~*~ Descending for removal: ' + newObj); this.removeSettingFromObj(obj[newObj], newElement); } } else this.logger.logMessage('*~*~*~ Not found for removal: ' + element); } if (changed) this.writePrefs(); } private removeSetting(objNames: string | Array) { this.removeSettingFromObj(this.prefsContainer, objNames); } public getSetting(objName: string) { console.log('Preferences.getSetting(' + objName + ')'); return this.prefsContainer[objName]; } }