diff --git a/.eslintrc.js b/.eslintrc.js index e7351df94..09f68949c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,29 +2,24 @@ module.exports = { settings: { - 'import/core-modules': [ - 'electron' - ] + 'import/core-modules': ['electron'], }, - extends: [ - 'airbnb-base', - 'prettier', - ], + extends: ['airbnb-base', 'prettier'], - plugins: [ - 'mocha', - 'more', - ], + plugins: ['mocha', 'more'], rules: { - 'comma-dangle': ['error', { + 'comma-dangle': [ + 'error', + { arrays: 'always-multiline', objects: 'always-multiline', imports: 'always-multiline', exports: 'always-multiline', functions: 'never', - }], + }, + ], // prevents us from accidentally checking in exclusive tests (`.only`): 'mocha/no-exclusive-tests': 'error', @@ -44,7 +39,11 @@ module.exports = { // consistently place operators at end of line except ternaries 'operator-linebreak': 'error', - 'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }], + quotes: [ + 'error', + 'single', + { avoidEscape: true, allowTemplateLiterals: false }, + ], // Prettier overrides: 'arrow-parens': 'off', diff --git a/Gruntfile.js b/Gruntfile.js index 78045eaad..0d2d5cbc6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -13,11 +13,13 @@ module.exports = function(grunt) { var libtextsecurecomponents = []; for (i in bower.concat.libtextsecure) { - libtextsecurecomponents.push('components/' + bower.concat.libtextsecure[i] + '/**/*.js'); + libtextsecurecomponents.push( + 'components/' + bower.concat.libtextsecure[i] + '/**/*.js' + ); } - var importOnce = require("node-sass-import-once"); - grunt.loadNpmTasks("grunt-sass"); + var importOnce = require('node-sass-import-once'); + grunt.loadNpmTasks('grunt-sass'); grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), @@ -34,15 +36,15 @@ module.exports = function(grunt) { src: [ 'components/mocha/mocha.js', 'components/chai/chai.js', - 'test/_test.js' + 'test/_test.js', ], dest: 'test/test.js', }, //TODO: Move errors back down? libtextsecure: { options: { - banner: ";(function() {\n", - footer: "})();\n", + banner: ';(function() {\n', + footer: '})();\n', }, src: [ 'libtextsecure/errors.js', @@ -77,21 +79,21 @@ module.exports = function(grunt) { 'components/mock-socket/dist/mock-socket.js', 'components/mocha/mocha.js', 'components/chai/chai.js', - 'libtextsecure/test/_test.js' + 'libtextsecure/test/_test.js', ], dest: 'libtextsecure/test/test.js', - } + }, }, sass: { options: { sourceMap: true, - importer: importOnce + importer: importOnce, }, dev: { files: { - "stylesheets/manifest.css": "stylesheets/manifest.scss" - } - } + 'stylesheets/manifest.css': 'stylesheets/manifest.scss', + }, + }, }, jshint: { files: [ @@ -117,7 +119,7 @@ module.exports = function(grunt) { '!js/models/messages.js', '!js/WebAudioRecorderMp3.js', '!libtextsecure/message_receiver.js', - '_locales/**/*' + '_locales/**/*', ], options: { jshintrc: '.jshintrc' }, }, @@ -130,135 +132,157 @@ module.exports = function(grunt) { 'protos/*', 'js/**', 'stylesheets/*.css', - '!js/register.js' + '!js/register.js', ], - res: [ - 'images/**/*', - 'fonts/*', - ] + res: ['images/**/*', 'fonts/*'], }, copy: { deps: { - files: [{ - src: 'components/mp3lameencoder/lib/Mp3LameEncoder.js', - dest: 'js/Mp3LameEncoder.min.js' - }, { - src: 'components/webaudiorecorder/lib/WebAudioRecorderMp3.js', - dest: 'js/WebAudioRecorderMp3.js' - }, { - src: 'components/jquery/dist/jquery.js', - dest: 'js/jquery.js' - }], + files: [ + { + src: 'components/mp3lameencoder/lib/Mp3LameEncoder.js', + dest: 'js/Mp3LameEncoder.min.js', + }, + { + src: 'components/webaudiorecorder/lib/WebAudioRecorderMp3.js', + dest: 'js/WebAudioRecorderMp3.js', + }, + { + src: 'components/jquery/dist/jquery.js', + dest: 'js/jquery.js', + }, + ], }, res: { files: [{ expand: true, dest: 'dist/', src: ['<%= dist.res %>'] }], }, src: { files: [{ expand: true, dest: 'dist/', src: ['<%= dist.src %>'] }], - } + }, }, jscs: { all: { src: [ - 'Gruntfile', - 'js/**/*.js', - '!js/components.js', - '!js/libsignal-protocol-worker.js', - '!js/libtextsecure.js', - '!js/modules/**/*.js', - '!js/models/conversations.js', - '!js/models/messages.js', - '!js/views/conversation_search_view.js', - '!js/views/conversation_view.js', - '!js/views/debug_log_view.js', - '!js/views/file_input_view.js', - '!js/views/message_view.js', - '!js/Mp3LameEncoder.min.js', - '!js/WebAudioRecorderMp3.js', - 'test/**/*.js', - '!test/blanket_mocha.js', - '!test/modules/**/*.js', - '!test/test.js', - ] - } + 'Gruntfile', + 'js/**/*.js', + '!js/components.js', + '!js/libsignal-protocol-worker.js', + '!js/libtextsecure.js', + '!js/modules/**/*.js', + '!js/models/conversations.js', + '!js/models/messages.js', + '!js/views/conversation_search_view.js', + '!js/views/conversation_view.js', + '!js/views/debug_log_view.js', + '!js/views/file_input_view.js', + '!js/views/message_view.js', + '!js/Mp3LameEncoder.min.js', + '!js/WebAudioRecorderMp3.js', + 'test/**/*.js', + '!test/blanket_mocha.js', + '!test/modules/**/*.js', + '!test/test.js', + ], + }, }, watch: { sass: { files: ['./stylesheets/*.scss'], - tasks: ['sass'] + tasks: ['sass'], }, libtextsecure: { files: ['./libtextsecure/*.js', './libtextsecure/storage/*.js'], - tasks: ['concat:libtextsecure'] + tasks: ['concat:libtextsecure'], }, dist: { files: ['<%= dist.src %>', '<%= dist.res %>'], - tasks: ['copy_dist'] + tasks: ['copy_dist'], }, scripts: { files: ['<%= jshint.files %>'], - tasks: ['jshint'] + tasks: ['jshint'], }, style: { files: ['<%= jscs.all.src %>'], - tasks: ['jscs'] + tasks: ['jscs'], }, transpile: { files: ['./ts/**/*.ts'], - tasks: ['exec:transpile'] - } + tasks: ['exec:transpile'], + }, }, exec: { 'tx-pull': { - cmd: 'tx pull' + cmd: 'tx pull', }, - 'transpile': { + transpile: { cmd: 'npm run transpile', - } + }, }, 'test-release': { osx: { - archive: 'mac/' + packageJson.productName + '.app/Contents/Resources/app.asar', - appUpdateYML: 'mac/' + packageJson.productName + '.app/Contents/Resources/app-update.yml', - exe: 'mac/' + packageJson.productName + '.app/Contents/MacOS/' + packageJson.productName + archive: + 'mac/' + packageJson.productName + '.app/Contents/Resources/app.asar', + appUpdateYML: + 'mac/' + + packageJson.productName + + '.app/Contents/Resources/app-update.yml', + exe: + 'mac/' + + packageJson.productName + + '.app/Contents/MacOS/' + + packageJson.productName, }, mas: { archive: 'mas/Signal.app/Contents/Resources/app.asar', appUpdateYML: 'mac/Signal.app/Contents/Resources/app-update.yml', - exe: 'mas/' + packageJson.productName + '.app/Contents/MacOS/' + packageJson.productName + exe: + 'mas/' + + packageJson.productName + + '.app/Contents/MacOS/' + + packageJson.productName, }, linux: { archive: 'linux-unpacked/resources/app.asar', - exe: 'linux-unpacked/' + packageJson.name + exe: 'linux-unpacked/' + packageJson.name, }, win: { archive: 'win-unpacked/resources/app.asar', appUpdateYML: 'win-unpacked/resources/app-update.yml', - exe: 'win-unpacked/' + packageJson.productName + '.exe' - } + exe: 'win-unpacked/' + packageJson.productName + '.exe', + }, }, - gitinfo: {} // to be populated by grunt gitinfo + gitinfo: {}, // to be populated by grunt gitinfo }); Object.keys(grunt.config.get('pkg').devDependencies).forEach(function(key) { - if (/^grunt(?!(-cli)?$)/.test(key)) { // ignore grunt and grunt-cli + if (/^grunt(?!(-cli)?$)/.test(key)) { + // ignore grunt and grunt-cli grunt.loadNpmTasks(key); } }); // Transifex does not understand placeholders, so this task patches all non-en // locales with missing placeholders - grunt.registerTask('locale-patch', function(){ + grunt.registerTask('locale-patch', function() { var en = grunt.file.readJSON('_locales/en/messages.json'); - grunt.file.recurse('_locales', function(abspath, rootdir, subdir, filename){ - if (subdir === 'en' || filename !== 'messages.json'){ + grunt.file.recurse('_locales', function( + abspath, + rootdir, + subdir, + filename + ) { + if (subdir === 'en' || filename !== 'messages.json') { return; } var messages = grunt.file.readJSON(abspath); - for (var key in messages){ - if (en[key] !== undefined && messages[key] !== undefined){ - if (en[key].placeholders !== undefined && messages[key].placeholders === undefined){ + for (var key in messages) { + if (en[key] !== undefined && messages[key] !== undefined) { + if ( + en[key].placeholders !== undefined && + messages[key].placeholders === undefined + ) { messages[key].placeholders = en[key].placeholders; } } @@ -269,12 +293,14 @@ module.exports = function(grunt) { }); grunt.registerTask('getExpireTime', function() { - grunt.task.requires('gitinfo'); - var gitinfo = grunt.config.get('gitinfo'); - var commited = gitinfo.local.branch.current.lastCommitTime; - var time = Date.parse(commited) + 1000 * 60 * 60 * 24 * 90; - grunt.file.write('config/local-production.json', - JSON.stringify({ buildExpiration: time }) + '\n'); + grunt.task.requires('gitinfo'); + var gitinfo = grunt.config.get('gitinfo'); + var commited = gitinfo.local.branch.current.lastCommitTime; + var time = Date.parse(commited) + 1000 * 60 * 60 * 24 * 90; + grunt.file.write( + 'config/local-production.json', + JSON.stringify({ buildExpiration: time }) + '\n' + ); }); grunt.registerTask('clean-release', function() { @@ -290,51 +316,62 @@ module.exports = function(grunt) { var gitinfo = grunt.config.get('gitinfo'); var https = require('https'); - var urlBase = "https://s3-us-west-1.amazonaws.com/signal-desktop-builds"; + var urlBase = 'https://s3-us-west-1.amazonaws.com/signal-desktop-builds'; var keyBase = 'signalapp/Signal-Desktop'; var sha = gitinfo.local.branch.current.SHA; - var files = [{ - zip: packageJson.name + '-' + packageJson.version + '.zip', - extractedTo: 'linux' - }]; + var files = [ + { + zip: packageJson.name + '-' + packageJson.version + '.zip', + extractedTo: 'linux', + }, + ]; var extract = require('extract-zip'); var download = function(url, dest, extractedTo, cb) { - var file = fs.createWriteStream(dest); - var request = https.get(url, function(response) { + var file = fs.createWriteStream(dest); + var request = https + .get(url, function(response) { if (response.statusCode !== 200) { cb(response.statusCode); } else { response.pipe(file); file.on('finish', function() { file.close(function() { - extract(dest, {dir: path.join(__dirname, 'release', extractedTo)}, cb); + extract( + dest, + { dir: path.join(__dirname, 'release', extractedTo) }, + cb + ); }); }); } - }).on('error', function(err) { // Handle errors + }) + .on('error', function(err) { + // Handle errors fs.unlink(dest); // Delete the file async. (But we don't check the result) if (cb) cb(err.message); }); }; - Promise.all(files.map(function(item) { - var key = [ keyBase, sha, 'dist', item.zip].join('/'); - var url = [urlBase, key].join('/'); - var dest = 'release/' + item.zip; - return new Promise(function(resolve) { - console.log(url); - download(url, dest, item.extractedTo, function(err) { - if (err) { - console.log('failed', dest, err); - resolve(err); - } else { - console.log('done', dest); - resolve(); - } + Promise.all( + files.map(function(item) { + var key = [keyBase, sha, 'dist', item.zip].join('/'); + var url = [urlBase, key].join('/'); + var dest = 'release/' + item.zip; + return new Promise(function(resolve) { + console.log(url); + download(url, dest, item.extractedTo, function(err) { + if (err) { + console.log('failed', dest, err); + resolve(err); + } else { + console.log('done', dest); + resolve(); + } + }); }); - }); - })).then(function(results) { + }) + ).then(function(results) { results.forEach(function(error) { if (error) { grunt.fail.warn('Failed to fetch some release artifacts'); @@ -347,65 +384,83 @@ module.exports = function(grunt) { function runTests(environment, cb) { var failure; var Application = require('spectron').Application; - var electronBinary = process.platform === 'win32' ? 'electron.cmd' : 'electron'; + var electronBinary = + process.platform === 'win32' ? 'electron.cmd' : 'electron'; var app = new Application({ path: path.join(__dirname, 'node_modules', '.bin', electronBinary), args: [path.join(__dirname, 'main.js')], env: { - NODE_ENV: environment - } + NODE_ENV: environment, + }, }); function getMochaResults() { return window.mochaResults; } - app.start().then(function() { - return app.client.waitUntil(function() { - return app.client.execute(getMochaResults).then(function(data) { - return Boolean(data.value); - }); - }, 10000, 'Expected to find window.mochaResults set!'); - }).then(function() { - return app.client.execute(getMochaResults); - }).then(function(data) { - var results = data.value; - if (results.failures > 0) { - console.error(results.reports); + app + .start() + .then(function() { + return app.client.waitUntil( + function() { + return app.client.execute(getMochaResults).then(function(data) { + return Boolean(data.value); + }); + }, + 10000, + 'Expected to find window.mochaResults set!' + ); + }) + .then(function() { + return app.client.execute(getMochaResults); + }) + .then(function(data) { + var results = data.value; + if (results.failures > 0) { + console.error(results.reports); + failure = function() { + grunt.fail.fatal( + 'Found ' + results.failures + ' failing unit tests.' + ); + }; + return app.client.log('browser'); + } else { + grunt.log.ok(results.passes + ' tests passed.'); + } + }) + .then(function(logs) { + if (logs) { + console.error(); + console.error('Because tests failed, printing browser logs:'); + console.error(logs); + } + }) + .catch(function(error) { failure = function() { - grunt.fail.fatal('Found ' + results.failures + ' failing unit tests.'); + grunt.fail.fatal( + 'Something went wrong: ' + error.message + ' ' + error.stack + ); }; - return app.client.log('browser'); - } else { - grunt.log.ok(results.passes + ' tests passed.'); - } - }).then(function(logs) { - if (logs) { - console.error(); - console.error('Because tests failed, printing browser logs:'); - console.error(logs); - } - }).catch(function (error) { - failure = function() { - grunt.fail.fatal('Something went wrong: ' + error.message + ' ' + error.stack); - }; - }).then(function () { - // We need to use the failure variable and this early stop to clean up before - // shutting down. Grunt's fail methods are the only way to set the return value, - // but they shut the process down immediately! - return app.stop(); - }).then(function() { - if (failure) { - failure(); - } - cb(); - }).catch(function (error) { - console.error('Second-level error:', error.message, error.stack); - if (failure) { - failure(); - } - cb(); - }); + }) + .then(function() { + // We need to use the failure variable and this early stop to clean up before + // shutting down. Grunt's fail methods are the only way to set the return value, + // but they shut the process down immediately! + return app.stop(); + }) + .then(function() { + if (failure) { + failure(); + } + cb(); + }) + .catch(function(error) { + console.error('Second-level error:', error.message, error.stack); + if (failure) { + failure(); + } + cb(); + }); } grunt.registerTask('unit-tests', 'Run unit tests w/Electron', function() { @@ -415,80 +470,99 @@ module.exports = function(grunt) { runTests(environment, done); }); - grunt.registerTask('lib-unit-tests', 'Run libtextsecure unit tests w/Electron', function() { - var environment = grunt.option('env') || 'test-lib'; - var done = this.async(); + grunt.registerTask( + 'lib-unit-tests', + 'Run libtextsecure unit tests w/Electron', + function() { + var environment = grunt.option('env') || 'test-lib'; + var done = this.async(); - runTests(environment, done); - }); + runTests(environment, done); + } + ); grunt.registerMultiTask('test-release', 'Test packaged releases', function() { - var dir = grunt.option('dir') || 'dist'; - var environment = grunt.option('env') || 'production'; - var asar = require('asar'); - var config = this.data; - var archive = [dir, config.archive].join('/'); - var files = [ - 'config/default.json', - 'config/' + environment + '.json', - 'config/local-' + environment + '.json' - ]; + var dir = grunt.option('dir') || 'dist'; + var environment = grunt.option('env') || 'production'; + var asar = require('asar'); + var config = this.data; + var archive = [dir, config.archive].join('/'); + var files = [ + 'config/default.json', + 'config/' + environment + '.json', + 'config/local-' + environment + '.json', + ]; - console.log(this.target, archive); - var releaseFiles = files.concat(config.files || []); - releaseFiles.forEach(function(fileName) { - console.log(fileName); - try { - asar.statFile(archive, fileName); - return true; - } catch (e) { - console.log(e); - throw new Error("Missing file " + fileName); - } - }); - - if (config.appUpdateYML) { - var appUpdateYML = [dir, config.appUpdateYML].join('/'); - if (require('fs').existsSync(appUpdateYML)) { - console.log("auto update ok"); - } else { - throw new Error("Missing auto update config " + appUpdateYML); - } + console.log(this.target, archive); + var releaseFiles = files.concat(config.files || []); + releaseFiles.forEach(function(fileName) { + console.log(fileName); + try { + asar.statFile(archive, fileName); + return true; + } catch (e) { + console.log(e); + throw new Error('Missing file ' + fileName); } + }); - var done = this.async(); - // A simple test to verify a visible window is opened with a title - var Application = require('spectron').Application; - var assert = require('assert'); + if (config.appUpdateYML) { + var appUpdateYML = [dir, config.appUpdateYML].join('/'); + if (require('fs').existsSync(appUpdateYML)) { + console.log('auto update ok'); + } else { + throw new Error('Missing auto update config ' + appUpdateYML); + } + } - var app = new Application({ - path: [dir, config.exe].join('/') - }); + var done = this.async(); + // A simple test to verify a visible window is opened with a title + var Application = require('spectron').Application; + var assert = require('assert'); - app.start().then(function () { + var app = new Application({ + path: [dir, config.exe].join('/'), + }); + + app + .start() + .then(function() { return app.client.getWindowCount(); - }).then(function (count) { + }) + .then(function(count) { assert.equal(count, 1); console.log('window opened'); - }).then(function () { + }) + .then(function() { // Get the window's title return app.client.getTitle(); - }).then(function (title) { + }) + .then(function(title) { // Verify the window's title assert.equal(title, packageJson.productName); console.log('title ok'); - }).then(function () { - assert(app.chromeDriver.logLines.indexOf('NODE_ENV ' + environment) > -1); + }) + .then(function() { + assert( + app.chromeDriver.logLines.indexOf('NODE_ENV ' + environment) > -1 + ); console.log('environment ok'); - }).then(function () { - // Successfully completed test - return app.stop(); - }, function (error) { - // Test failed! - return app.stop().then(function() { - grunt.fail.fatal('Test failed: ' + error.message + ' ' + error.stack); - }); - }).then(done); + }) + .then( + function() { + // Successfully completed test + return app.stop(); + }, + function(error) { + // Test failed! + return app.stop().then(function() { + grunt.fail.fatal( + 'Test failed: ' + error.message + ' ' + error.stack + ); + }); + } + ) + .then(done); }); grunt.registerTask('tx', ['exec:tx-pull', 'locale-patch']); @@ -497,9 +571,16 @@ module.exports = function(grunt) { grunt.registerTask('test', ['unit-tests', 'lib-unit-tests']); grunt.registerTask('copy_dist', ['gitinfo', 'copy:res', 'copy:src']); grunt.registerTask('date', ['gitinfo', 'getExpireTime']); - grunt.registerTask('prep-release', ['gitinfo', 'clean-release', 'fetch-release']); - grunt.registerTask( - 'default', - ['concat', 'copy:deps', 'sass', 'date', 'exec:transpile'] - ); + grunt.registerTask('prep-release', [ + 'gitinfo', + 'clean-release', + 'fetch-release', + ]); + grunt.registerTask('default', [ + 'concat', + 'copy:deps', + 'sass', + 'date', + 'exec:transpile', + ]); }; diff --git a/js/background.js b/js/background.js index 4a5ed347c..5c0dc8878 100644 --- a/js/background.js +++ b/js/background.js @@ -11,70 +11,72 @@ /* global Whisper: false */ /* global wrapDeferred: false */ -;(async function() { - 'use strict'; +(async function() { + 'use strict'; - const { IdleDetector, MessageDataMigrator } = Signal.Workflow; - const { Errors, Message } = window.Signal.Types; - const { upgradeMessageSchema } = window.Signal.Migrations; - const { Migrations0DatabaseWithAttachmentData } = window.Signal.Migrations; - const { Views } = window.Signal; + const { IdleDetector, MessageDataMigrator } = Signal.Workflow; + const { Errors, Message } = window.Signal.Types; + const { upgradeMessageSchema } = window.Signal.Migrations; + const { Migrations0DatabaseWithAttachmentData } = window.Signal.Migrations; + const { Views } = window.Signal; - // Implicitly used in `indexeddb-backbonejs-adapter`: - // https://github.com/signalapp/Signal-Desktop/blob/4033a9f8137e62ed286170ed5d4941982b1d3a64/components/indexeddb-backbonejs-adapter/backbone-indexeddb.js#L569 - window.onInvalidStateError = function(e) { - console.log(e); - }; + // Implicitly used in `indexeddb-backbonejs-adapter`: + // https://github.com/signalapp/Signal-Desktop/blob/4033a9f8137e62ed286170ed5d4941982b1d3a64/components/indexeddb-backbonejs-adapter/backbone-indexeddb.js#L569 + window.onInvalidStateError = function(e) { + console.log(e); + }; - console.log('background page reloaded'); - console.log('environment:', window.config.environment); + console.log('background page reloaded'); + console.log('environment:', window.config.environment); - var initialLoadComplete = false; - window.owsDesktopApp = {}; + var initialLoadComplete = false; + window.owsDesktopApp = {}; - var title = window.config.name; - if (window.config.environment !== 'production') { - title += ' - ' + window.config.environment; + var title = window.config.name; + if (window.config.environment !== 'production') { + title += ' - ' + window.config.environment; + } + if (window.config.appInstance) { + title += ' - ' + window.config.appInstance; + } + window.config.title = window.document.title = title; + + // start a background worker for ecc + textsecure.startWorker('js/libsignal-protocol-worker.js'); + Whisper.KeyChangeListener.init(textsecure.storage.protocol); + textsecure.storage.protocol.on('removePreKey', function() { + getAccountManager().refreshPreKeys(); + }); + + var SERVER_URL = window.config.serverUrl; + var CDN_URL = window.config.cdnUrl; + var messageReceiver; + window.getSocketStatus = function() { + if (messageReceiver) { + return messageReceiver.getStatus(); + } else { + return -1; } - if (window.config.appInstance) { - title += ' - ' + window.config.appInstance; + }; + Whisper.events = _.clone(Backbone.Events); + var accountManager; + window.getAccountManager = function() { + if (!accountManager) { + var USERNAME = storage.get('number_id'); + var PASSWORD = storage.get('password'); + accountManager = new textsecure.AccountManager( + SERVER_URL, + USERNAME, + PASSWORD + ); + accountManager.addEventListener('registration', function() { + Whisper.Registration.markDone(); + console.log('dispatching registration event'); + Whisper.events.trigger('registration_done'); + }); } - window.config.title = window.document.title = title; - - // start a background worker for ecc - textsecure.startWorker('js/libsignal-protocol-worker.js'); - Whisper.KeyChangeListener.init(textsecure.storage.protocol); - textsecure.storage.protocol.on('removePreKey', function() { - getAccountManager().refreshPreKeys(); - }); - - var SERVER_URL = window.config.serverUrl; - var CDN_URL = window.config.cdnUrl; - var messageReceiver; - window.getSocketStatus = function() { - if (messageReceiver) { - return messageReceiver.getStatus(); - } else { - return -1; - } - }; - Whisper.events = _.clone(Backbone.Events); - var accountManager; - window.getAccountManager = function() { - if (!accountManager) { - var USERNAME = storage.get('number_id'); - var PASSWORD = storage.get('password'); - accountManager = new textsecure.AccountManager( - SERVER_URL, USERNAME, PASSWORD - ); - accountManager.addEventListener('registration', function() { - Whisper.Registration.markDone(); - console.log('dispatching registration event'); - Whisper.events.trigger('registration_done'); - }); - } - return accountManager; - }; + return accountManager; + }; /* eslint-enable */ const cancelInitializationMessage = Views.Initialization.setMessage(); @@ -105,275 +107,293 @@ if (!isMigrationWithoutIndexComplete) { const database = Migrations0DatabaseWithAttachmentData.getDatabase(); - const batchWithoutIndex = await MessageDataMigrator.processNextBatchWithoutIndex({ - databaseName: database.name, - minDatabaseVersion: database.version, - numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, - upgradeMessageSchema, - }); + const batchWithoutIndex = await MessageDataMigrator.processNextBatchWithoutIndex( + { + databaseName: database.name, + minDatabaseVersion: database.version, + numMessagesPerBatch: NUM_MESSAGES_PER_BATCH, + upgradeMessageSchema, + } + ); console.log('Upgrade message schema (without index):', batchWithoutIndex); isMigrationWithoutIndexComplete = batchWithoutIndex.done; } - const areAllMigrationsComplete = isMigrationWithIndexComplete && - isMigrationWithoutIndexComplete; + const areAllMigrationsComplete = + isMigrationWithIndexComplete && isMigrationWithoutIndexComplete; if (areAllMigrationsComplete) { idleDetector.stop(); } }); /* eslint-disable */ - // We need this 'first' check because we don't want to start the app up any other time - // than the first time. And storage.fetch() will cause onready() to fire. - var first = true; - storage.onready(function() { - if (!first) { - return; - } - first = false; + // We need this 'first' check because we don't want to start the app up any other time + // than the first time. And storage.fetch() will cause onready() to fire. + var first = true; + storage.onready(function() { + if (!first) { + return; + } + first = false; - ConversationController.load().then(start, start); - }); + ConversationController.load().then(start, start); + }); - Whisper.events.on('shutdown', function() { - idleDetector.stop(); + Whisper.events.on('shutdown', function() { + idleDetector.stop(); - if (messageReceiver) { - messageReceiver.close().then(function() { - Whisper.events.trigger('shutdown-complete'); - }); - } else { + if (messageReceiver) { + messageReceiver.close().then(function() { Whisper.events.trigger('shutdown-complete'); - } - }); + }); + } else { + Whisper.events.trigger('shutdown-complete'); + } + }); - Whisper.events.on('setupWithImport', function() { - var appView = window.owsDesktopApp.appView; - if (appView) { - appView.openImporter(); - } - }); + Whisper.events.on('setupWithImport', function() { + var appView = window.owsDesktopApp.appView; + if (appView) { + appView.openImporter(); + } + }); - Whisper.events.on('setupAsNewDevice', function() { - var appView = window.owsDesktopApp.appView; - if (appView) { - appView.openInstaller(); - } - }); + Whisper.events.on('setupAsNewDevice', function() { + var appView = window.owsDesktopApp.appView; + if (appView) { + appView.openInstaller(); + } + }); - Whisper.events.on('setupAsStandalone', function() { - var appView = window.owsDesktopApp.appView; - if (appView) { - appView.openStandalone(); - } - }); + Whisper.events.on('setupAsStandalone', function() { + var appView = window.owsDesktopApp.appView; + if (appView) { + appView.openStandalone(); + } + }); - function start() { - var currentVersion = window.config.version; - var lastVersion = storage.get('version'); - var newVersion = !lastVersion || currentVersion !== lastVersion; - storage.put('version', currentVersion); + function start() { + var currentVersion = window.config.version; + var lastVersion = storage.get('version'); + var newVersion = !lastVersion || currentVersion !== lastVersion; + storage.put('version', currentVersion); - if (newVersion) { - console.log('New version detected:', currentVersion); - } - - window.dispatchEvent(new Event('storage_ready')); - - console.log('listening for registration events'); - Whisper.events.on('registration_done', function() { - console.log('handling registration event'); - Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion); - connect(true); - }); - - cancelInitializationMessage(); - var appView = window.owsDesktopApp.appView = new Whisper.AppView({el: $('body')}); - - Whisper.WallClockListener.init(Whisper.events); - Whisper.ExpiringMessagesListener.init(Whisper.events); - - if (Whisper.Import.isIncomplete()) { - console.log('Import was interrupted, showing import error screen'); - appView.openImporter(); - } else if (Whisper.Registration.everDone()) { - Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion); - connect(); - appView.openInbox({ - initialLoadComplete: initialLoadComplete - }); - } else if (window.config.importMode) { - appView.openImporter(); - } else { - appView.openInstaller(); - } - - Whisper.events.on('showDebugLog', function() { - appView.openDebugLog(); - }); - Whisper.events.on('showSettings', () => { - if (!appView || !appView.inboxView) { - console.log( - 'background: Event: \'showSettings\':' + - ' Expected `appView.inboxView` to exist.' - ); - return; - } - appView.inboxView.showSettings(); - }); - Whisper.events.on('unauthorized', function() { - appView.inboxView.networkStatusView.update(); - }); - Whisper.events.on('reconnectTimer', function() { - appView.inboxView.networkStatusView.setSocketReconnectInterval(60000); - }); - Whisper.events.on('contactsync', function() { - if (appView.installView) { - appView.openInbox(); - } - }); - Whisper.Notifications.on('click', function(conversation) { - showWindow(); - if (conversation) { - appView.openConversation(conversation); - } else { - appView.openInbox({ - initialLoadComplete: initialLoadComplete - }); - } - }); + if (newVersion) { + console.log('New version detected:', currentVersion); } - window.getSyncRequest = function() { - return new textsecure.SyncRequest(textsecure.messaging, messageReceiver); + window.dispatchEvent(new Event('storage_ready')); + + console.log('listening for registration events'); + Whisper.events.on('registration_done', function() { + console.log('handling registration event'); + Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion); + connect(true); + }); + + cancelInitializationMessage(); + var appView = (window.owsDesktopApp.appView = new Whisper.AppView({ + el: $('body'), + })); + + Whisper.WallClockListener.init(Whisper.events); + Whisper.ExpiringMessagesListener.init(Whisper.events); + + if (Whisper.Import.isIncomplete()) { + console.log('Import was interrupted, showing import error screen'); + appView.openImporter(); + } else if (Whisper.Registration.everDone()) { + Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion); + connect(); + appView.openInbox({ + initialLoadComplete: initialLoadComplete, + }); + } else if (window.config.importMode) { + appView.openImporter(); + } else { + appView.openInstaller(); + } + + Whisper.events.on('showDebugLog', function() { + appView.openDebugLog(); + }); + Whisper.events.on('showSettings', () => { + if (!appView || !appView.inboxView) { + console.log( + "background: Event: 'showSettings':" + + ' Expected `appView.inboxView` to exist.' + ); + return; + } + appView.inboxView.showSettings(); + }); + Whisper.events.on('unauthorized', function() { + appView.inboxView.networkStatusView.update(); + }); + Whisper.events.on('reconnectTimer', function() { + appView.inboxView.networkStatusView.setSocketReconnectInterval(60000); + }); + Whisper.events.on('contactsync', function() { + if (appView.installView) { + appView.openInbox(); + } + }); + Whisper.Notifications.on('click', function(conversation) { + showWindow(); + if (conversation) { + appView.openConversation(conversation); + } else { + appView.openInbox({ + initialLoadComplete: initialLoadComplete, + }); + } + }); + } + + window.getSyncRequest = function() { + return new textsecure.SyncRequest(textsecure.messaging, messageReceiver); + }; + + Whisper.events.on('start-shutdown', function() { + if (messageReceiver) { + messageReceiver.close().then(function() { + Whisper.events.trigger('shutdown-complete'); + }); + } else { + Whisper.events.trigger('shutdown-complete'); + } + }); + + var disconnectTimer = null; + function onOffline() { + console.log('offline'); + + window.removeEventListener('offline', onOffline); + window.addEventListener('online', onOnline); + + // We've received logs from Linux where we get an 'offline' event, then 30ms later + // we get an online event. This waits a bit after getting an 'offline' event + // before disconnecting the socket manually. + disconnectTimer = setTimeout(disconnect, 1000); + } + + function onOnline() { + console.log('online'); + + window.removeEventListener('online', onOnline); + window.addEventListener('offline', onOffline); + + if (disconnectTimer && isSocketOnline()) { + console.log('Already online. Had a blip in online/offline status.'); + clearTimeout(disconnectTimer); + disconnectTimer = null; + return; + } + if (disconnectTimer) { + clearTimeout(disconnectTimer); + disconnectTimer = null; + } + + connect(); + } + + function isSocketOnline() { + var socketStatus = window.getSocketStatus(); + return ( + socketStatus === WebSocket.CONNECTING || socketStatus === WebSocket.OPEN + ); + } + + function disconnect() { + console.log('disconnect'); + + // Clear timer, since we're only called when the timer is expired + disconnectTimer = null; + + if (messageReceiver) { + messageReceiver.close(); + } + } + + var connectCount = 0; + async function connect(firstRun) { + console.log('connect'); + + // Bootstrap our online/offline detection, only the first time we connect + if (connectCount === 0 && navigator.onLine) { + window.addEventListener('offline', onOffline); + } + if (connectCount === 0 && !navigator.onLine) { + console.log( + 'Starting up offline; will connect when we have network access' + ); + window.addEventListener('online', onOnline); + onEmpty(); // this ensures that the loading screen is dismissed + return; + } + + if (!Whisper.Registration.everDone()) { + return; + } + if (Whisper.Import.isIncomplete()) { + return; + } + + if (messageReceiver) { + messageReceiver.close(); + } + + var USERNAME = storage.get('number_id'); + var PASSWORD = storage.get('password'); + var mySignalingKey = storage.get('signaling_key'); + + connectCount += 1; + var options = { + retryCached: connectCount === 1, }; - Whisper.events.on('start-shutdown', function() { - if (messageReceiver) { - messageReceiver.close().then(function() { - Whisper.events.trigger('shutdown-complete'); - }); - } else { - Whisper.events.trigger('shutdown-complete'); + Whisper.Notifications.disable(); // avoid notification flood until empty + + // initialize the socket and start listening for messages + messageReceiver = new textsecure.MessageReceiver( + SERVER_URL, + USERNAME, + PASSWORD, + mySignalingKey, + options + ); + messageReceiver.addEventListener('message', onMessageReceived); + messageReceiver.addEventListener('delivery', onDeliveryReceipt); + messageReceiver.addEventListener('contact', onContactReceived); + messageReceiver.addEventListener('group', onGroupReceived); + messageReceiver.addEventListener('sent', onSentMessage); + messageReceiver.addEventListener('readSync', onReadSync); + messageReceiver.addEventListener('read', onReadReceipt); + messageReceiver.addEventListener('verified', onVerified); + messageReceiver.addEventListener('error', onError); + messageReceiver.addEventListener('empty', onEmpty); + messageReceiver.addEventListener('progress', onProgress); + messageReceiver.addEventListener('configuration', onConfiguration); + + window.textsecure.messaging = new textsecure.MessageSender( + SERVER_URL, + USERNAME, + PASSWORD, + CDN_URL + ); + + // Because v0.43.2 introduced a bug that lost contact details, v0.43.4 introduces + // a one-time contact sync to restore all lost contact/group information. We + // disable this checking if a user is first registering. + var key = 'chrome-contact-sync-v0.43.4'; + if (!storage.get(key)) { + storage.put(key, true); + + if (!firstRun && textsecure.storage.user.getDeviceId() != '1') { + window.getSyncRequest(); } - }); - - - var disconnectTimer = null; - function onOffline() { - console.log('offline'); - - window.removeEventListener('offline', onOffline); - window.addEventListener('online', onOnline); - - // We've received logs from Linux where we get an 'offline' event, then 30ms later - // we get an online event. This waits a bit after getting an 'offline' event - // before disconnecting the socket manually. - disconnectTimer = setTimeout(disconnect, 1000); } - function onOnline() { - console.log('online'); - - window.removeEventListener('online', onOnline); - window.addEventListener('offline', onOffline); - - if (disconnectTimer && isSocketOnline()) { - console.log('Already online. Had a blip in online/offline status.'); - clearTimeout(disconnectTimer); - disconnectTimer = null; - return; - } - if (disconnectTimer) { - clearTimeout(disconnectTimer); - disconnectTimer = null; - } - - connect(); - } - - function isSocketOnline() { - var socketStatus = window.getSocketStatus(); - return socketStatus === WebSocket.CONNECTING || socketStatus === WebSocket.OPEN; - } - - function disconnect() { - console.log('disconnect'); - - // Clear timer, since we're only called when the timer is expired - disconnectTimer = null; - - if (messageReceiver) { - messageReceiver.close(); - } - } - - var connectCount = 0; - async function connect(firstRun) { - console.log('connect'); - - // Bootstrap our online/offline detection, only the first time we connect - if (connectCount === 0 && navigator.onLine) { - window.addEventListener('offline', onOffline); - } - if (connectCount === 0 && !navigator.onLine) { - console.log('Starting up offline; will connect when we have network access'); - window.addEventListener('online', onOnline); - onEmpty(); // this ensures that the loading screen is dismissed - return; - } - - if (!Whisper.Registration.everDone()) { return; } - if (Whisper.Import.isIncomplete()) { return; } - - if (messageReceiver) { - messageReceiver.close(); - } - - var USERNAME = storage.get('number_id'); - var PASSWORD = storage.get('password'); - var mySignalingKey = storage.get('signaling_key'); - - connectCount += 1; - var options = { - retryCached: connectCount === 1, - }; - - Whisper.Notifications.disable(); // avoid notification flood until empty - - // initialize the socket and start listening for messages - messageReceiver = new textsecure.MessageReceiver( - SERVER_URL, USERNAME, PASSWORD, mySignalingKey, options - ); - messageReceiver.addEventListener('message', onMessageReceived); - messageReceiver.addEventListener('delivery', onDeliveryReceipt); - messageReceiver.addEventListener('contact', onContactReceived); - messageReceiver.addEventListener('group', onGroupReceived); - messageReceiver.addEventListener('sent', onSentMessage); - messageReceiver.addEventListener('readSync', onReadSync); - messageReceiver.addEventListener('read', onReadReceipt); - messageReceiver.addEventListener('verified', onVerified); - messageReceiver.addEventListener('error', onError); - messageReceiver.addEventListener('empty', onEmpty); - messageReceiver.addEventListener('progress', onProgress); - messageReceiver.addEventListener('configuration', onConfiguration); - - window.textsecure.messaging = new textsecure.MessageSender( - SERVER_URL, USERNAME, PASSWORD, CDN_URL - ); - - // Because v0.43.2 introduced a bug that lost contact details, v0.43.4 introduces - // a one-time contact sync to restore all lost contact/group information. We - // disable this checking if a user is first registering. - var key = 'chrome-contact-sync-v0.43.4'; - if (!storage.get(key)) { - storage.put(key, true); - - if (!firstRun && textsecure.storage.user.getDeviceId() != '1') { - window.getSyncRequest(); - } - } - /* eslint-enable */ const deviceId = textsecure.storage.user.getDeviceId(); const { sendRequestConfigurationSyncMessage } = textsecure.messaging; @@ -406,7 +426,7 @@ }); if (Whisper.Import.isComplete()) { - textsecure.messaging.sendRequestConfigurationSyncMessage().catch((e) => { + textsecure.messaging.sendRequestConfigurationSyncMessage().catch(e => { console.log(e); }); } @@ -416,8 +436,10 @@ const shouldSkipAttachmentMigrationForNewUsers = firstRun === true; if (shouldSkipAttachmentMigrationForNewUsers) { const database = Migrations0DatabaseWithAttachmentData.getDatabase(); - const connection = - await Signal.Database.open(database.name, database.version); + const connection = await Signal.Database.open( + database.name, + database.version + ); await Signal.Settings.markAttachmentMigrationComplete(connection); } idleDetector.start(); @@ -425,176 +447,181 @@ } /* eslint-disable */ - function onChangeTheme() { - var view = window.owsDesktopApp.appView; - if (view) { - view.applyTheme(); + function onChangeTheme() { + var view = window.owsDesktopApp.appView; + if (view) { + view.applyTheme(); + } + } + function onEmpty() { + initialLoadComplete = true; + + var interval = setInterval(function() { + var view = window.owsDesktopApp.appView; + if (view) { + clearInterval(interval); + interval = null; + view.onEmpty(); + } + }, 500); + + Whisper.Notifications.enable(); + } + function onProgress(ev) { + var count = ev.count; + + var view = window.owsDesktopApp.appView; + if (view) { + view.onProgress(count); + } + } + function onConfiguration(ev) { + storage.put('read-receipt-setting', ev.configuration.readReceipts); + } + + function onContactReceived(ev) { + var details = ev.contactDetails; + + var id = details.number; + + if (id === textsecure.storage.user.getNumber()) { + // special case for syncing details about ourselves + if (details.profileKey) { + console.log('Got sync message with our own profile key'); + storage.put('profileKey', details.profileKey); + } + } + + var c = new Whisper.Conversation({ + id: id, + }); + var error = c.validateNumber(); + if (error) { + console.log('Invalid contact received:', Errors.toLogFormat(error)); + return; + } + + return ConversationController.getOrCreateAndWait(id, 'private') + .then(function(conversation) { + var activeAt = conversation.get('active_at'); + + // The idea is to make any new contact show up in the left pane. If + // activeAt is null, then this contact has been purposefully hidden. + if (activeAt !== null) { + activeAt = activeAt || Date.now(); } - } - function onEmpty() { - initialLoadComplete = true; - var interval = setInterval(function() { - var view = window.owsDesktopApp.appView; - if (view) { - clearInterval(interval); - interval = null; - view.onEmpty(); - } - }, 500); - - Whisper.Notifications.enable(); - } - function onProgress(ev) { - var count = ev.count; - - var view = window.owsDesktopApp.appView; - if (view) { - view.onProgress(count); + if (details.profileKey) { + conversation.set({ profileKey: details.profileKey }); } - } - function onConfiguration(ev) { - storage.put('read-receipt-setting', ev.configuration.readReceipts); - } - function onContactReceived(ev) { - var details = ev.contactDetails; - - var id = details.number; - - if (id === textsecure.storage.user.getNumber()) { - // special case for syncing details about ourselves - if (details.profileKey) { - console.log('Got sync message with our own profile key'); - storage.put('profileKey', details.profileKey); + if (typeof details.blocked !== 'undefined') { + if (details.blocked) { + storage.addBlockedNumber(id); + } else { + storage.removeBlockedNumber(id); } } - var c = new Whisper.Conversation({ - id: id - }); - var error = c.validateNumber(); - if (error) { - console.log('Invalid contact received:', Errors.toLogFormat(error)); + return wrapDeferred( + conversation.save({ + name: details.name, + avatar: details.avatar, + color: details.color, + active_at: activeAt, + }) + ).then(function() { + const { expireTimer } = details; + const isValidExpireTimer = typeof expireTimer === 'number'; + if (!isValidExpireTimer) { + console.log( + 'Ignore invalid expire timer.', + 'Expected numeric `expireTimer`, got:', + expireTimer + ); return; - } + } - return ConversationController.getOrCreateAndWait(id, 'private') - .then(function(conversation) { - var activeAt = conversation.get('active_at'); - - // The idea is to make any new contact show up in the left pane. If - // activeAt is null, then this contact has been purposefully hidden. - if (activeAt !== null) { - activeAt = activeAt || Date.now(); - } - - if (details.profileKey) { - conversation.set({profileKey: details.profileKey}); - } - - if (typeof details.blocked !== 'undefined') { - if (details.blocked) { - storage.addBlockedNumber(id); - } else { - storage.removeBlockedNumber(id); - } - } - - return wrapDeferred(conversation.save({ - name: details.name, - avatar: details.avatar, - color: details.color, - active_at: activeAt, - })).then(function() { - const { expireTimer } = details; - const isValidExpireTimer = typeof expireTimer === 'number'; - if (!isValidExpireTimer) { - console.log( - 'Ignore invalid expire timer.', - 'Expected numeric `expireTimer`, got:', expireTimer - ); - return; - } - - var source = textsecure.storage.user.getNumber(); - var receivedAt = Date.now(); - return conversation.updateExpirationTimer( - expireTimer, - source, - receivedAt, - {fromSync: true} - ); - }); - }) - .then(function() { - if (details.verified) { - var verified = details.verified; - var ev = new Event('verified'); - ev.verified = { - state: verified.state, - destination: verified.destination, - identityKey: verified.identityKey.toArrayBuffer(), - }; - ev.viaContactSync = true; - return onVerified(ev); - } - }) - .then(ev.confirm) - .catch(function(error) { - console.log( - 'onContactReceived error:', - Errors.toLogFormat(error) - ); - }); - } - - function onGroupReceived(ev) { - var details = ev.groupDetails; - var id = details.id; - - return ConversationController.getOrCreateAndWait(id, 'group').then(function(conversation) { - var updates = { - name: details.name, - members: details.members, - avatar: details.avatar, - type: 'group', - }; - if (details.active) { - var activeAt = conversation.get('active_at'); - - // The idea is to make any new group show up in the left pane. If - // activeAt is null, then this group has been purposefully hidden. - if (activeAt !== null) { - updates.active_at = activeAt || Date.now(); - } - updates.left = false; - } else { - updates.left = true; - } - - return wrapDeferred(conversation.save(updates)).then(function() { - const { expireTimer } = details; - const isValidExpireTimer = typeof expireTimer === 'number'; - if (!isValidExpireTimer) { - console.log( - 'Ignore invalid expire timer.', - 'Expected numeric `expireTimer`, got:', expireTimer - ); - return; - } - - var source = textsecure.storage.user.getNumber(); - var receivedAt = Date.now(); - return conversation.updateExpirationTimer( - expireTimer, - source, - receivedAt, - {fromSync: true} - ); - }).then(ev.confirm); + var source = textsecure.storage.user.getNumber(); + var receivedAt = Date.now(); + return conversation.updateExpirationTimer( + expireTimer, + source, + receivedAt, + { fromSync: true } + ); }); - } + }) + .then(function() { + if (details.verified) { + var verified = details.verified; + var ev = new Event('verified'); + ev.verified = { + state: verified.state, + destination: verified.destination, + identityKey: verified.identityKey.toArrayBuffer(), + }; + ev.viaContactSync = true; + return onVerified(ev); + } + }) + .then(ev.confirm) + .catch(function(error) { + console.log('onContactReceived error:', Errors.toLogFormat(error)); + }); + } + + function onGroupReceived(ev) { + var details = ev.groupDetails; + var id = details.id; + + return ConversationController.getOrCreateAndWait(id, 'group').then(function( + conversation + ) { + var updates = { + name: details.name, + members: details.members, + avatar: details.avatar, + type: 'group', + }; + if (details.active) { + var activeAt = conversation.get('active_at'); + + // The idea is to make any new group show up in the left pane. If + // activeAt is null, then this group has been purposefully hidden. + if (activeAt !== null) { + updates.active_at = activeAt || Date.now(); + } + updates.left = false; + } else { + updates.left = true; + } + + return wrapDeferred(conversation.save(updates)) + .then(function() { + const { expireTimer } = details; + const isValidExpireTimer = typeof expireTimer === 'number'; + if (!isValidExpireTimer) { + console.log( + 'Ignore invalid expire timer.', + 'Expected numeric `expireTimer`, got:', + expireTimer + ); + return; + } + + var source = textsecure.storage.user.getNumber(); + var receivedAt = Date.now(); + return conversation.updateExpirationTimer( + expireTimer, + source, + receivedAt, + { fromSync: true } + ); + }) + .then(ev.confirm); + }); + } /* eslint-enable */ @@ -605,25 +632,23 @@ }); // Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`: - const getDescriptorForSent = ({ message, destination }) => ( + const getDescriptorForSent = ({ message, destination }) => message.group ? getGroupDescriptor(message.group) - : { type: Message.PRIVATE, id: destination } - ); + : { type: Message.PRIVATE, id: destination }; // Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`: - const getDescriptorForReceived = ({ message, source }) => ( + const getDescriptorForReceived = ({ message, source }) => message.group ? getGroupDescriptor(message.group) - : { type: Message.PRIVATE, id: source } - ); + : { type: Message.PRIVATE, id: source }; function createMessageHandler({ createMessage, getMessageDescriptor, handleProfileUpdate, }) { - return async (event) => { + return async event => { const { data, confirm } = event; const messageDescriptor = getMessageDescriptor(data); @@ -647,11 +672,9 @@ messageDescriptor.id, messageDescriptor.type ); - return message.handleDataMessage( - upgradedMessage, - event.confirm, - { initialLoadComplete } - ); + return message.handleDataMessage(upgradedMessage, event.confirm, { + initialLoadComplete, + }); }; } @@ -677,7 +700,10 @@ }); // Sent: - async function handleMessageSentProfileUpdate({ confirm, messageDescriptor }) { + async function handleMessageSentProfileUpdate({ + confirm, + messageDescriptor, + }) { const conversation = await ConversationController.getOrCreateAndWait( messageDescriptor.id, messageDescriptor.type @@ -707,233 +733,249 @@ }); /* eslint-disable */ - function isMessageDuplicate(message) { - return new Promise(function(resolve) { - var fetcher = new Whisper.Message(); - var options = { - index: { - name: 'unique', - value: [ - message.get('source'), - message.get('sourceDevice'), - message.get('sent_at') - ] - } - }; + function isMessageDuplicate(message) { + return new Promise(function(resolve) { + var fetcher = new Whisper.Message(); + var options = { + index: { + name: 'unique', + value: [ + message.get('source'), + message.get('sourceDevice'), + message.get('sent_at'), + ], + }, + }; - fetcher.fetch(options).always(function() { - if (fetcher.get('id')) { - return resolve(true); - } + fetcher.fetch(options).always(function() { + if (fetcher.get('id')) { + return resolve(true); + } - return resolve(false); - }); - }).catch(function(error) { - console.log('isMessageDuplicate error:', Errors.toLogFormat(error)); - return false; - }); + return resolve(false); + }); + }).catch(function(error) { + console.log('isMessageDuplicate error:', Errors.toLogFormat(error)); + return false; + }); + } + + function initIncomingMessage(data) { + var message = new Whisper.Message({ + source: data.source, + sourceDevice: data.sourceDevice, + sent_at: data.timestamp, + received_at: data.receivedAt || Date.now(), + conversationId: data.source, + type: 'incoming', + unread: 1, + }); + + return message; + } + + function onError(ev) { + var error = ev.error; + console.log('background onError:', Errors.toLogFormat(error)); + + if ( + error.name === 'HTTPError' && + (error.code == 401 || error.code == 403) + ) { + Whisper.events.trigger('unauthorized'); + + console.log( + 'Client is no longer authorized; deleting local configuration' + ); + Whisper.Registration.remove(); + var previousNumberId = textsecure.storage.get('number_id'); + + textsecure.storage.protocol.removeAllConfiguration().then( + function() { + // These two bits of data are important to ensure that the app loads up + // the conversation list, instead of showing just the QR code screen. + Whisper.Registration.markEverDone(); + textsecure.storage.put('number_id', previousNumberId); + console.log('Successfully cleared local configuration'); + }, + function(error) { + console.log( + 'Something went wrong clearing local configuration', + error && error.stack ? error.stack : error + ); + } + ); + + return; } - function initIncomingMessage(data) { - var message = new Whisper.Message({ - source : data.source, - sourceDevice : data.sourceDevice, - sent_at : data.timestamp, - received_at : data.receivedAt || Date.now(), - conversationId : data.source, - type : 'incoming', - unread : 1 - }); + if (error.name === 'HTTPError' && error.code == -1) { + // Failed to connect to server + if (navigator.onLine) { + console.log('retrying in 1 minute'); + setTimeout(connect, 60000); - return message; + Whisper.events.trigger('reconnectTimer'); + } + return; } - function onError(ev) { - var error = ev.error; - console.log('background onError:', Errors.toLogFormat(error)); + if (ev.proto) { + if (error.name === 'MessageCounterError') { + if (ev.confirm) { + ev.confirm(); + } + // Ignore this message. It is likely a duplicate delivery + // because the server lost our ack the first time. + return; + } + var envelope = ev.proto; + var message = initIncomingMessage(envelope); - if (error.name === 'HTTPError' && (error.code == 401 || error.code == 403)) { - Whisper.events.trigger('unauthorized'); - - console.log('Client is no longer authorized; deleting local configuration'); - Whisper.Registration.remove(); - var previousNumberId = textsecure.storage.get('number_id'); - - textsecure.storage.protocol.removeAllConfiguration().then(function() { - // These two bits of data are important to ensure that the app loads up - // the conversation list, instead of showing just the QR code screen. - Whisper.Registration.markEverDone(); - textsecure.storage.put('number_id', previousNumberId); - console.log('Successfully cleared local configuration'); - }, function(error) { - console.log( - 'Something went wrong clearing local configuration', - error && error.stack ? error.stack : error - ); + return message.saveErrors(error).then(function() { + var id = message.get('conversationId'); + return ConversationController.getOrCreateAndWait(id, 'private').then( + function(conversation) { + conversation.set({ + active_at: Date.now(), + unreadCount: conversation.get('unreadCount') + 1, }); - return; - } - - if (error.name === 'HTTPError' && error.code == -1) { - // Failed to connect to server - if (navigator.onLine) { - console.log('retrying in 1 minute'); - setTimeout(connect, 60000); - - Whisper.events.trigger('reconnectTimer'); + var conversation_timestamp = conversation.get('timestamp'); + var message_timestamp = message.get('timestamp'); + if ( + !conversation_timestamp || + message_timestamp > conversation_timestamp + ) { + conversation.set({ timestamp: message.get('sent_at') }); } - return; - } - if (ev.proto) { - if (error.name === 'MessageCounterError') { - if (ev.confirm) { - ev.confirm(); - } - // Ignore this message. It is likely a duplicate delivery - // because the server lost our ack the first time. - return; + conversation.trigger('newmessage', message); + conversation.notify(message); + + if (ev.confirm) { + ev.confirm(); } - var envelope = ev.proto; - var message = initIncomingMessage(envelope); - return message.saveErrors(error).then(function() { - var id = message.get('conversationId'); - return ConversationController.getOrCreateAndWait(id, 'private').then(function(conversation) { - conversation.set({ - active_at: Date.now(), - unreadCount: conversation.get('unreadCount') + 1 - }); - - var conversation_timestamp = conversation.get('timestamp'); - var message_timestamp = message.get('timestamp'); - if (!conversation_timestamp || message_timestamp > conversation_timestamp) { - conversation.set({ timestamp: message.get('sent_at') }); - } - - conversation.trigger('newmessage', message); - conversation.notify(message); - - if (ev.confirm) { - ev.confirm(); - } - - return new Promise(function(resolve, reject) { - conversation.save().then(resolve, reject); - }); - }); + return new Promise(function(resolve, reject) { + conversation.save().then(resolve, reject); }); - } - - throw error; - } - - function onReadReceipt(ev) { - var read_at = ev.timestamp; - var timestamp = ev.read.timestamp; - var reader = ev.read.reader; - console.log('read receipt', reader, timestamp); - - if (!storage.get('read-receipt-setting')) { - return ev.confirm(); - } - - var receipt = Whisper.ReadReceipts.add({ - reader : reader, - timestamp : timestamp, - read_at : read_at, - }); - - receipt.on('remove', ev.confirm); - - // Calling this directly so we can wait for completion - return Whisper.ReadReceipts.onReceipt(receipt); - } - - function onReadSync(ev) { - var read_at = ev.timestamp; - var timestamp = ev.read.timestamp; - var sender = ev.read.sender; - console.log('read sync', sender, timestamp); - - var receipt = Whisper.ReadSyncs.add({ - sender : sender, - timestamp : timestamp, - read_at : read_at - }); - - receipt.on('remove', ev.confirm); - - // Calling this directly so we can wait for completion - return Whisper.ReadSyncs.onReceipt(receipt); - } - - function onVerified(ev) { - var number = ev.verified.destination; - var key = ev.verified.identityKey; - var state; - - var c = new Whisper.Conversation({ - id: number - }); - var error = c.validateNumber(); - if (error) { - console.log( - 'Invalid verified sync received:', - Errors.toLogFormat(error) - ); - return; - } - - switch(ev.verified.state) { - case textsecure.protobuf.Verified.State.DEFAULT: - state = 'DEFAULT'; - break; - case textsecure.protobuf.Verified.State.VERIFIED: - state = 'VERIFIED'; - break; - case textsecure.protobuf.Verified.State.UNVERIFIED: - state = 'UNVERIFIED'; - break; - } - - console.log('got verified sync for', number, state, - ev.viaContactSync ? 'via contact sync' : ''); - - return ConversationController.getOrCreateAndWait(number, 'private').then(function(contact) { - var options = { - viaSyncMessage: true, - viaContactSync: ev.viaContactSync, - key: key - }; - - if (state === 'VERIFIED') { - return contact.setVerified(options).then(ev.confirm); - } else if (state === 'DEFAULT') { - return contact.setVerifiedDefault(options).then(ev.confirm); - } else { - return contact.setUnverified(options).then(ev.confirm); - } - }); - } - - function onDeliveryReceipt(ev) { - var deliveryReceipt = ev.deliveryReceipt; - console.log( - 'delivery receipt from', - deliveryReceipt.source + '.' + deliveryReceipt.sourceDevice, - deliveryReceipt.timestamp + } ); - - var receipt = Whisper.DeliveryReceipts.add({ - timestamp: deliveryReceipt.timestamp, - source: deliveryReceipt.source - }); - - ev.confirm(); - - // Calling this directly so we can wait for completion - return Whisper.DeliveryReceipts.onReceipt(receipt); + }); } + + throw error; + } + + function onReadReceipt(ev) { + var read_at = ev.timestamp; + var timestamp = ev.read.timestamp; + var reader = ev.read.reader; + console.log('read receipt', reader, timestamp); + + if (!storage.get('read-receipt-setting')) { + return ev.confirm(); + } + + var receipt = Whisper.ReadReceipts.add({ + reader: reader, + timestamp: timestamp, + read_at: read_at, + }); + + receipt.on('remove', ev.confirm); + + // Calling this directly so we can wait for completion + return Whisper.ReadReceipts.onReceipt(receipt); + } + + function onReadSync(ev) { + var read_at = ev.timestamp; + var timestamp = ev.read.timestamp; + var sender = ev.read.sender; + console.log('read sync', sender, timestamp); + + var receipt = Whisper.ReadSyncs.add({ + sender: sender, + timestamp: timestamp, + read_at: read_at, + }); + + receipt.on('remove', ev.confirm); + + // Calling this directly so we can wait for completion + return Whisper.ReadSyncs.onReceipt(receipt); + } + + function onVerified(ev) { + var number = ev.verified.destination; + var key = ev.verified.identityKey; + var state; + + var c = new Whisper.Conversation({ + id: number, + }); + var error = c.validateNumber(); + if (error) { + console.log('Invalid verified sync received:', Errors.toLogFormat(error)); + return; + } + + switch (ev.verified.state) { + case textsecure.protobuf.Verified.State.DEFAULT: + state = 'DEFAULT'; + break; + case textsecure.protobuf.Verified.State.VERIFIED: + state = 'VERIFIED'; + break; + case textsecure.protobuf.Verified.State.UNVERIFIED: + state = 'UNVERIFIED'; + break; + } + + console.log( + 'got verified sync for', + number, + state, + ev.viaContactSync ? 'via contact sync' : '' + ); + + return ConversationController.getOrCreateAndWait(number, 'private').then( + function(contact) { + var options = { + viaSyncMessage: true, + viaContactSync: ev.viaContactSync, + key: key, + }; + + if (state === 'VERIFIED') { + return contact.setVerified(options).then(ev.confirm); + } else if (state === 'DEFAULT') { + return contact.setVerifiedDefault(options).then(ev.confirm); + } else { + return contact.setUnverified(options).then(ev.confirm); + } + } + ); + } + + function onDeliveryReceipt(ev) { + var deliveryReceipt = ev.deliveryReceipt; + console.log( + 'delivery receipt from', + deliveryReceipt.source + '.' + deliveryReceipt.sourceDevice, + deliveryReceipt.timestamp + ); + + var receipt = Whisper.DeliveryReceipts.add({ + timestamp: deliveryReceipt.timestamp, + source: deliveryReceipt.source, + }); + + ev.confirm(); + + // Calling this directly so we can wait for completion + return Whisper.DeliveryReceipts.onReceipt(receipt); + } })(); diff --git a/js/chromium.js b/js/chromium.js index f26bd9826..c35cd50b1 100644 --- a/js/chromium.js +++ b/js/chromium.js @@ -1,14 +1,14 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - // Browser specific functions for Chrom* - window.extension = window.extension || {}; +(function() { + 'use strict'; + // Browser specific functions for Chrom* + window.extension = window.extension || {}; - extension.windows = { - onClosed: function(callback) { - window.addEventListener('beforeunload', callback); - } - }; -}()); + extension.windows = { + onClosed: function(callback) { + window.addEventListener('beforeunload', callback); + }, + }; +})(); diff --git a/js/conversation_controller.js b/js/conversation_controller.js index 6696b0954..b8289214f 100644 --- a/js/conversation_controller.js +++ b/js/conversation_controller.js @@ -4,195 +4,215 @@ */ // This script should only be included in background.html -(function () { - 'use strict'; +(function() { + 'use strict'; - window.Whisper = window.Whisper || {}; + window.Whisper = window.Whisper || {}; - var conversations = new Whisper.ConversationCollection(); - var inboxCollection = new (Backbone.Collection.extend({ - initialize: function() { - this.on('change:timestamp change:name change:number', this.sort); + var conversations = new Whisper.ConversationCollection(); + var inboxCollection = new (Backbone.Collection.extend({ + initialize: function() { + this.on('change:timestamp change:name change:number', this.sort); - this.listenTo(conversations, 'add change:active_at', this.addActive); - this.listenTo(conversations, 'reset', function() { - this.reset([]); - }); + this.listenTo(conversations, 'add change:active_at', this.addActive); + this.listenTo(conversations, 'reset', function() { + this.reset([]); + }); - this.on('add remove change:unreadCount', - _.debounce(this.updateUnreadCount.bind(this), 1000) - ); - this.startPruning(); + this.on( + 'add remove change:unreadCount', + _.debounce(this.updateUnreadCount.bind(this), 1000) + ); + this.startPruning(); - this.collator = new Intl.Collator(); + this.collator = new Intl.Collator(); + }, + comparator: function(m1, m2) { + var timestamp1 = m1.get('timestamp'); + var timestamp2 = m2.get('timestamp'); + if (timestamp1 && !timestamp2) { + return -1; + } + if (timestamp2 && !timestamp1) { + return 1; + } + if (timestamp1 && timestamp2 && timestamp1 !== timestamp2) { + return timestamp2 - timestamp1; + } + + var title1 = m1.getTitle().toLowerCase(); + var title2 = m2.getTitle().toLowerCase(); + return this.collator.compare(title1, title2); + }, + addActive: function(model) { + if (model.get('active_at')) { + this.add(model); + } else { + this.remove(model); + } + }, + updateUnreadCount: function() { + var newUnreadCount = _.reduce( + this.map(function(m) { + return m.get('unreadCount'); + }), + function(item, memo) { + return item + memo; }, - comparator: function(m1, m2) { - var timestamp1 = m1.get('timestamp'); - var timestamp2 = m2.get('timestamp'); - if (timestamp1 && !timestamp2) { - return -1; - } - if (timestamp2 && !timestamp1) { - return 1; - } - if (timestamp1 && timestamp2 && timestamp1 !== timestamp2) { - return timestamp2 - timestamp1; - } + 0 + ); + storage.put('unreadCount', newUnreadCount); - var title1 = m1.getTitle().toLowerCase(); - var title2 = m2.getTitle().toLowerCase(); - return this.collator.compare(title1, title2); - }, - addActive: function(model) { - if (model.get('active_at')) { - this.add(model); - } else { - this.remove(model); - } - }, - updateUnreadCount: function() { - var newUnreadCount = _.reduce( - this.map(function(m) { return m.get('unreadCount'); }), - function(item, memo) { - return item + memo; - }, - 0 - ); - storage.put("unreadCount", newUnreadCount); + if (newUnreadCount > 0) { + window.setBadgeCount(newUnreadCount); + window.document.title = + window.config.title + ' (' + newUnreadCount + ')'; + } else { + window.setBadgeCount(0); + window.document.title = window.config.title; + } + window.updateTrayIcon(newUnreadCount); + }, + startPruning: function() { + var halfHour = 30 * 60 * 1000; + this.interval = setInterval( + function() { + this.forEach(function(conversation) { + conversation.trigger('prune'); + }); + }.bind(this), + halfHour + ); + }, + }))(); - if (newUnreadCount > 0) { - window.setBadgeCount(newUnreadCount); - window.document.title = window.config.title + " (" + newUnreadCount + ")"; - } else { - window.setBadgeCount(0); - window.document.title = window.config.title; - } - window.updateTrayIcon(newUnreadCount); - }, - startPruning: function() { - var halfHour = 30 * 60 * 1000; - this.interval = setInterval(function() { - this.forEach(function(conversation) { - conversation.trigger('prune'); - }); - }.bind(this), halfHour); + window.getInboxCollection = function() { + return inboxCollection; + }; + + window.ConversationController = { + get: function(id) { + if (!this._initialFetchComplete) { + throw new Error( + 'ConversationController.get() needs complete initial fetch' + ); + } + + return conversations.get(id); + }, + // Needed for some model setup which happens during the initial fetch() call below + getUnsafe: function(id) { + return conversations.get(id); + }, + dangerouslyCreateAndAdd: function(attributes) { + return conversations.add(attributes); + }, + getOrCreate: function(id, type) { + if (typeof id !== 'string') { + throw new TypeError("'id' must be a string"); + } + + if (type !== 'private' && type !== 'group') { + throw new TypeError( + `'type' must be 'private' or 'group'; got: '${type}'` + ); + } + + if (!this._initialFetchComplete) { + throw new Error( + 'ConversationController.get() needs complete initial fetch' + ); + } + + var conversation = conversations.get(id); + if (conversation) { + return conversation; + } + + conversation = conversations.add({ + id: id, + type: type, + }); + conversation.initialPromise = new Promise(function(resolve, reject) { + if (!conversation.isValid()) { + var validationError = conversation.validationError || {}; + console.log( + 'Contact is not valid. Not saving, but adding to collection:', + conversation.idForLogging(), + validationError.stack + ); + + return resolve(conversation); } - }))(); - window.getInboxCollection = function() { - return inboxCollection; - }; - - window.ConversationController = { - get: function(id) { - if (!this._initialFetchComplete) { - throw new Error('ConversationController.get() needs complete initial fetch'); - } - - return conversations.get(id); - }, - // Needed for some model setup which happens during the initial fetch() call below - getUnsafe: function(id) { - return conversations.get(id); - }, - dangerouslyCreateAndAdd: function(attributes) { - return conversations.add(attributes); - }, - getOrCreate: function(id, type) { - if (typeof id !== 'string') { - throw new TypeError("'id' must be a string"); - } - - if (type !== 'private' && type !== 'group') { - throw new TypeError(`'type' must be 'private' or 'group'; got: '${type}'`); - } - - if (!this._initialFetchComplete) { - throw new Error('ConversationController.get() needs complete initial fetch'); - } - - var conversation = conversations.get(id); - if (conversation) { - return conversation; - } - - conversation = conversations.add({ - id: id, - type: type - }); - conversation.initialPromise = new Promise(function(resolve, reject) { - if (!conversation.isValid()) { - var validationError = conversation.validationError || {}; - console.log( - 'Contact is not valid. Not saving, but adding to collection:', - conversation.idForLogging(), - validationError.stack - ); - - return resolve(conversation); - } - - var deferred = conversation.save(); - if (!deferred) { - console.log('Conversation save failed! ', id, type); - return reject(new Error('getOrCreate: Conversation save failed')); - } - - deferred.then(function() { - resolve(conversation); - }, reject); - }); - - return conversation; - }, - getOrCreateAndWait: function(id, type) { - return this._initialPromise.then(function() { - var conversation = this.getOrCreate(id, type); - - if (conversation) { - return conversation.initialPromise.then(function() { - return conversation; - }); - } - - return Promise.reject( - new Error('getOrCreateAndWait: did not get conversation') - ); - }.bind(this)); - }, - getAllGroupsInvolvingId: function(id) { - var groups = new Whisper.GroupCollection(); - return groups.fetchGroups(id).then(function() { - return groups.map(function(group) { - return conversations.add(group); - }); - }); - }, - loadPromise: function() { - return this._initialPromise; - }, - reset: function() { - this._initialPromise = Promise.resolve(); - conversations.reset([]); - }, - load: function() { - console.log('ConversationController: starting initial fetch'); - - this._initialPromise = new Promise(function(resolve, reject) { - conversations.fetch().then(function() { - console.log('ConversationController: done with initial fetch'); - this._initialFetchComplete = true; - resolve(); - }.bind(this), function(error) { - console.log( - 'ConversationController: initial fetch failed', - error && error.stack ? error.stack : error - ); - reject(error); - }); - }.bind(this)); - - return this._initialPromise; + var deferred = conversation.save(); + if (!deferred) { + console.log('Conversation save failed! ', id, type); + return reject(new Error('getOrCreate: Conversation save failed')); } - }; + + deferred.then(function() { + resolve(conversation); + }, reject); + }); + + return conversation; + }, + getOrCreateAndWait: function(id, type) { + return this._initialPromise.then( + function() { + var conversation = this.getOrCreate(id, type); + + if (conversation) { + return conversation.initialPromise.then(function() { + return conversation; + }); + } + + return Promise.reject( + new Error('getOrCreateAndWait: did not get conversation') + ); + }.bind(this) + ); + }, + getAllGroupsInvolvingId: function(id) { + var groups = new Whisper.GroupCollection(); + return groups.fetchGroups(id).then(function() { + return groups.map(function(group) { + return conversations.add(group); + }); + }); + }, + loadPromise: function() { + return this._initialPromise; + }, + reset: function() { + this._initialPromise = Promise.resolve(); + conversations.reset([]); + }, + load: function() { + console.log('ConversationController: starting initial fetch'); + + this._initialPromise = new Promise( + function(resolve, reject) { + conversations.fetch().then( + function() { + console.log('ConversationController: done with initial fetch'); + this._initialFetchComplete = true; + resolve(); + }.bind(this), + function(error) { + console.log( + 'ConversationController: initial fetch failed', + error && error.stack ? error.stack : error + ); + reject(error); + } + ); + }.bind(this) + ); + + return this._initialPromise; + }, + }; })(); diff --git a/js/database.js b/js/database.js index a5688e8c4..6bade25f8 100644 --- a/js/database.js +++ b/js/database.js @@ -3,7 +3,7 @@ /* global _: false */ // eslint-disable-next-line func-names -(function () { +(function() { 'use strict'; const { getPlaceholderMigrations } = window.Signal.Migrations; @@ -24,13 +24,13 @@ }; function clearStores(db, names) { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { const storeNames = names || db.objectStoreNames; console.log('Clearing these indexeddb stores:', storeNames); const transaction = db.transaction(storeNames, 'readwrite'); let finished = false; - const finish = (via) => { + const finish = via => { console.log('clearing all stores done via', via); if (finished) { resolve(); @@ -50,7 +50,7 @@ let count = 0; // can't use built-in .forEach because db.objectStoreNames is not a plain array - _.forEach(storeNames, (storeName) => { + _.forEach(storeNames, storeName => { const store = transaction.objectStore(storeName); const request = store.clear(); @@ -72,7 +72,7 @@ ); }; }); - })); + }); } Whisper.Database.open = () => { @@ -80,7 +80,7 @@ const { version } = migrations[migrations.length - 1]; const DBOpenRequest = window.indexedDB.open(Whisper.Database.id, version); - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { // these two event handlers act on the IDBDatabase object, // when the database is opened successfully, or not DBOpenRequest.onerror = reject; @@ -91,7 +91,7 @@ // been created before, or a new version number has been // submitted via the window.indexedDB.open line above DBOpenRequest.onupgradeneeded = reject; - })); + }); }; Whisper.Database.clear = async () => { @@ -99,7 +99,7 @@ return clearStores(db); }; - Whisper.Database.clearStores = async (storeNames) => { + Whisper.Database.clearStores = async storeNames => { const db = await Whisper.Database.open(); return clearStores(db, storeNames); }; @@ -107,7 +107,7 @@ Whisper.Database.close = () => window.wrapDeferred(Backbone.sync('closeall')); Whisper.Database.drop = () => - new Promise(((resolve, reject) => { + new Promise((resolve, reject) => { const request = window.indexedDB.deleteDatabase(Whisper.Database.id); request.onblocked = () => { @@ -121,7 +121,7 @@ }; request.onsuccess = resolve; - })); + }); Whisper.Database.migrations = getPlaceholderMigrations(); -}()); +})(); diff --git a/js/delivery_receipts.js b/js/delivery_receipts.js index e4289a57f..4cce1f2a4 100644 --- a/js/delivery_receipts.js +++ b/js/delivery_receipts.js @@ -1,79 +1,105 @@ /* * vim: ts=4:sw=4:expandtab */ -;(function() { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.DeliveryReceipts = new (Backbone.Collection.extend({ - forMessage: function(conversation, message) { - var recipients; - if (conversation.isPrivate()) { - recipients = [ conversation.id ]; - } else { - recipients = conversation.get('members') || []; - } - var receipts = this.filter(function(receipt) { - return (receipt.get('timestamp') === message.get('sent_at')) && - (recipients.indexOf(receipt.get('source')) > -1); + Whisper.DeliveryReceipts = new (Backbone.Collection.extend({ + forMessage: function(conversation, message) { + var recipients; + if (conversation.isPrivate()) { + recipients = [conversation.id]; + } else { + recipients = conversation.get('members') || []; + } + var receipts = this.filter(function(receipt) { + return ( + receipt.get('timestamp') === message.get('sent_at') && + recipients.indexOf(receipt.get('source')) > -1 + ); + }); + this.remove(receipts); + return receipts; + }, + onReceipt: function(receipt) { + var messages = new Whisper.MessageCollection(); + return messages + .fetchSentAt(receipt.get('timestamp')) + .then(function() { + if (messages.length === 0) { + return; + } + var message = messages.find(function(message) { + return ( + !message.isIncoming() && + receipt.get('source') === message.get('conversationId') + ); + }); + if (message) { + return message; + } + + var groups = new Whisper.GroupCollection(); + return groups.fetchGroups(receipt.get('source')).then(function() { + var ids = groups.pluck('id'); + ids.push(receipt.get('source')); + return messages.find(function(message) { + return ( + !message.isIncoming() && + _.contains(ids, message.get('conversationId')) + ); }); - this.remove(receipts); - return receipts; - }, - onReceipt: function(receipt) { - var messages = new Whisper.MessageCollection(); - return messages.fetchSentAt(receipt.get('timestamp')).then(function() { - if (messages.length === 0) { return; } - var message = messages.find(function(message) { - return (!message.isIncoming() && receipt.get('source') === message.get('conversationId')); - }); - if (message) { return message; } - - var groups = new Whisper.GroupCollection(); - return groups.fetchGroups(receipt.get('source')).then(function() { - var ids = groups.pluck('id'); - ids.push(receipt.get('source')); - return messages.find(function(message) { - return (!message.isIncoming() && - _.contains(ids, message.get('conversationId'))); - }); - }); - }).then(function(message) { - if (message) { - var deliveries = message.get('delivered') || 0; - var delivered_to = message.get('delivered_to') || []; - return new Promise(function(resolve, reject) { - message.save({ - delivered_to: _.union(delivered_to, [receipt.get('source')]), - delivered: deliveries + 1 - }).then(function() { - // notify frontend listeners - var conversation = ConversationController.get( - message.get('conversationId') - ); - if (conversation) { - conversation.trigger('delivered', message); - } - - this.remove(receipt); - resolve(); - }.bind(this), reject); - }.bind(this)); - // TODO: consider keeping a list of numbers we've - // successfully delivered to? - } else { - console.log( - 'No message for delivery receipt', + }); + }) + .then( + function(message) { + if (message) { + var deliveries = message.get('delivered') || 0; + var delivered_to = message.get('delivered_to') || []; + return new Promise( + function(resolve, reject) { + message + .save({ + delivered_to: _.union(delivered_to, [ receipt.get('source'), - receipt.get('timestamp') + ]), + delivered: deliveries + 1, + }) + .then( + function() { + // notify frontend listeners + var conversation = ConversationController.get( + message.get('conversationId') + ); + if (conversation) { + conversation.trigger('delivered', message); + } + + this.remove(receipt); + resolve(); + }.bind(this), + reject ); - } - }.bind(this)).catch(function(error) { - console.log( - 'DeliveryReceipts.onReceipt error:', - error && error.stack ? error.stack : error - ); - }); - } - }))(); + }.bind(this) + ); + // TODO: consider keeping a list of numbers we've + // successfully delivered to? + } else { + console.log( + 'No message for delivery receipt', + receipt.get('source'), + receipt.get('timestamp') + ); + } + }.bind(this) + ) + .catch(function(error) { + console.log( + 'DeliveryReceipts.onReceipt error:', + error && error.stack ? error.stack : error + ); + }); + }, + }))(); })(); diff --git a/js/emoji_util.js b/js/emoji_util.js index 76dbf75e3..289339480 100644 --- a/js/emoji_util.js +++ b/js/emoji_util.js @@ -2,98 +2,94 @@ * vim: ts=4:sw=4:expandtab */ -;(function() { - 'use strict'; - window.emoji_util = window.emoji_util || {}; +(function() { + 'use strict'; + window.emoji_util = window.emoji_util || {}; - // EmojiConverter overrides - EmojiConvertor.prototype.getCountOfAllMatches = function(str, regex) { - var match = regex.exec(str); - var count = 0; + // EmojiConverter overrides + EmojiConvertor.prototype.getCountOfAllMatches = function(str, regex) { + var match = regex.exec(str); + var count = 0; - if (!regex.global) { - return match ? 1 : 0; - } + if (!regex.global) { + return match ? 1 : 0; + } - while (match) { - count += 1; - match = regex.exec(str); - } + while (match) { + count += 1; + match = regex.exec(str); + } - return count; - }; + return count; + }; - EmojiConvertor.prototype.hasNormalCharacters = function(str) { - var self = this; - var noEmoji = str.replace(self.rx_unified, '').trim(); - return noEmoji.length > 0; - }; + EmojiConvertor.prototype.hasNormalCharacters = function(str) { + var self = this; + var noEmoji = str.replace(self.rx_unified, '').trim(); + return noEmoji.length > 0; + }; - EmojiConvertor.prototype.getSizeClass = function(str) { - var self = this; + EmojiConvertor.prototype.getSizeClass = function(str) { + var self = this; - if (self.hasNormalCharacters(str)) { - return ''; - } + if (self.hasNormalCharacters(str)) { + return ''; + } - var emojiCount = self.getCountOfAllMatches(str, self.rx_unified); - if (emojiCount > 8) { - return ''; - } - else if (emojiCount > 6) { - return 'small'; - } - else if (emojiCount > 4) { - return 'medium'; - } - else if (emojiCount > 2) { - return 'large'; - } - else { - return 'jumbo'; - } - }; + var emojiCount = self.getCountOfAllMatches(str, self.rx_unified); + if (emojiCount > 8) { + return ''; + } else if (emojiCount > 6) { + return 'small'; + } else if (emojiCount > 4) { + return 'medium'; + } else if (emojiCount > 2) { + return 'large'; + } else { + return 'jumbo'; + } + }; - var imgClass = /(]+ class="emoji)(")/g; - EmojiConvertor.prototype.addClass = function(text, sizeClass) { - if (!sizeClass) { - return text; - } + var imgClass = /(]+ class="emoji)(")/g; + EmojiConvertor.prototype.addClass = function(text, sizeClass) { + if (!sizeClass) { + return text; + } - return text.replace(imgClass, function(match, before, after) { - return before + ' ' + sizeClass + after; - }); - }; + return text.replace(imgClass, function(match, before, after) { + return before + ' ' + sizeClass + after; + }); + }; - var imgTitle = /(]+ class="emoji[^>]+ title=")([^:">]+)(")/g; - EmojiConvertor.prototype.ensureTitlesHaveColons = function(text) { - return text.replace(imgTitle, function(match, before, title, after) { - return before + ':' + title + ':' + after; - }); - }; + var imgTitle = /(]+ class="emoji[^>]+ title=")([^:">]+)(")/g; + EmojiConvertor.prototype.ensureTitlesHaveColons = function(text) { + return text.replace(imgTitle, function(match, before, title, after) { + return before + ':' + title + ':' + after; + }); + }; - EmojiConvertor.prototype.signalReplace = function(str) { - var sizeClass = this.getSizeClass(str); + EmojiConvertor.prototype.signalReplace = function(str) { + var sizeClass = this.getSizeClass(str); - var text = this.replace_unified(str); - text = this.addClass(text, sizeClass); + var text = this.replace_unified(str); + text = this.addClass(text, sizeClass); - return this.ensureTitlesHaveColons(text); - }; + return this.ensureTitlesHaveColons(text); + }; - window.emoji = new EmojiConvertor(); - emoji.init_colons(); - emoji.img_sets.apple.path = 'node_modules/emoji-datasource-apple/img/apple/64/'; - emoji.include_title = true; - emoji.replace_mode = 'img'; - emoji.supports_css = false; // needed to avoid spans with background-image + window.emoji = new EmojiConvertor(); + emoji.init_colons(); + emoji.img_sets.apple.path = + 'node_modules/emoji-datasource-apple/img/apple/64/'; + emoji.include_title = true; + emoji.replace_mode = 'img'; + emoji.supports_css = false; // needed to avoid spans with background-image - window.emoji_util.parse = function($el) { - if (!$el || !$el.length) { - return; - } - - $el.html(emoji.signalReplace($el.html())); - }; + window.emoji_util.parse = function($el) { + if (!$el || !$el.length) { + return; + } + $el.html(emoji.signalReplace($el.html())); + }; })(); diff --git a/js/expire.js b/js/expire.js index 4e5e2d0da..cf18a0ddc 100644 --- a/js/expire.js +++ b/js/expire.js @@ -1,16 +1,16 @@ -;(function() { - 'use strict'; - var BUILD_EXPIRATION = 0; - try { - BUILD_EXPIRATION = parseInt(window.config.buildExpiration); - if (BUILD_EXPIRATION) { - console.log("Build expires: ", new Date(BUILD_EXPIRATION).toISOString()); - } - } catch (e) {} +(function() { + 'use strict'; + var BUILD_EXPIRATION = 0; + try { + BUILD_EXPIRATION = parseInt(window.config.buildExpiration); + if (BUILD_EXPIRATION) { + console.log('Build expires: ', new Date(BUILD_EXPIRATION).toISOString()); + } + } catch (e) {} - window.extension = window.extension || {}; + window.extension = window.extension || {}; - extension.expired = function() { - return (BUILD_EXPIRATION && Date.now() > BUILD_EXPIRATION); - }; + extension.expired = function() { + return BUILD_EXPIRATION && Date.now() > BUILD_EXPIRATION; + }; })(); diff --git a/js/expiring_messages.js b/js/expiring_messages.js index 327658c16..5230daa5b 100644 --- a/js/expiring_messages.js +++ b/js/expiring_messages.js @@ -1,115 +1,124 @@ - /* * vim: ts=4:sw=4:expandtab */ -;(function() { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - function destroyExpiredMessages() { - // Load messages that have expired and destroy them - var expired = new Whisper.MessageCollection(); - expired.on('add', function(message) { - console.log('message', message.get('sent_at'), 'expired'); - var conversation = message.getConversation(); - if (conversation) { - conversation.trigger('expired', message); - } - - // We delete after the trigger to allow the conversation time to process - // the expiration before the message is removed from the database. - message.destroy(); - }); - expired.on('reset', throttledCheckExpiringMessages); - - expired.fetchExpired(); - } - - var timeout; - function checkExpiringMessages() { - // Look up the next expiring message and set a timer to destroy it - var expiring = new Whisper.MessageCollection(); - expiring.once('add', function(next) { - var expires_at = next.get('expires_at'); - console.log('next message expires', new Date(expires_at).toISOString()); - - var wait = expires_at - Date.now(); - - // In the past - if (wait < 0) { wait = 0; } - - // Too far in the future, since it's limited to a 32-bit value - if (wait > 2147483647) { wait = 2147483647; } - - clearTimeout(timeout); - timeout = setTimeout(destroyExpiredMessages, wait); - }); - expiring.fetchNextExpiring(); - } - var throttledCheckExpiringMessages = _.throttle(checkExpiringMessages, 1000); - - Whisper.ExpiringMessagesListener = { - init: function(events) { - checkExpiringMessages(); - events.on('timetravel', throttledCheckExpiringMessages); - }, - update: throttledCheckExpiringMessages - }; - - var TimerOption = Backbone.Model.extend({ - getName: function() { - return i18n([ - 'timerOption', this.get('time'), this.get('unit'), - ].join('_')) || moment.duration(this.get('time'), this.get('unit')).humanize(); - }, - getAbbreviated: function() { - return i18n([ - 'timerOption', this.get('time'), this.get('unit'), 'abbreviated' - ].join('_')); + function destroyExpiredMessages() { + // Load messages that have expired and destroy them + var expired = new Whisper.MessageCollection(); + expired.on('add', function(message) { + console.log('message', message.get('sent_at'), 'expired'); + var conversation = message.getConversation(); + if (conversation) { + conversation.trigger('expired', message); } + + // We delete after the trigger to allow the conversation time to process + // the expiration before the message is removed from the database. + message.destroy(); }); - Whisper.ExpirationTimerOptions = new (Backbone.Collection.extend({ - model: TimerOption, - getName: function(seconds) { - if (!seconds) { - seconds = 0; - } - var o = this.findWhere({seconds: seconds}); - if (o) { return o.getName(); } - else { - return [seconds, 'seconds'].join(' '); - } - }, - getAbbreviated: function(seconds) { - if (!seconds) { - seconds = 0; - } - var o = this.findWhere({seconds: seconds}); - if (o) { return o.getAbbreviated(); } - else { - return [seconds, 's'].join(''); - } + expired.on('reset', throttledCheckExpiringMessages); + + expired.fetchExpired(); + } + + var timeout; + function checkExpiringMessages() { + // Look up the next expiring message and set a timer to destroy it + var expiring = new Whisper.MessageCollection(); + expiring.once('add', function(next) { + var expires_at = next.get('expires_at'); + console.log('next message expires', new Date(expires_at).toISOString()); + + var wait = expires_at - Date.now(); + + // In the past + if (wait < 0) { + wait = 0; } - }))([ - [ 0, 'seconds' ], - [ 5, 'seconds' ], - [ 10, 'seconds' ], - [ 30, 'seconds' ], - [ 1, 'minute' ], - [ 5, 'minutes' ], - [ 30, 'minutes' ], - [ 1, 'hour' ], - [ 6, 'hours' ], - [ 12, 'hours' ], - [ 1, 'day' ], - [ 1, 'week' ], + + // Too far in the future, since it's limited to a 32-bit value + if (wait > 2147483647) { + wait = 2147483647; + } + + clearTimeout(timeout); + timeout = setTimeout(destroyExpiredMessages, wait); + }); + expiring.fetchNextExpiring(); + } + var throttledCheckExpiringMessages = _.throttle(checkExpiringMessages, 1000); + + Whisper.ExpiringMessagesListener = { + init: function(events) { + checkExpiringMessages(); + events.on('timetravel', throttledCheckExpiringMessages); + }, + update: throttledCheckExpiringMessages, + }; + + var TimerOption = Backbone.Model.extend({ + getName: function() { + return ( + i18n(['timerOption', this.get('time'), this.get('unit')].join('_')) || + moment.duration(this.get('time'), this.get('unit')).humanize() + ); + }, + getAbbreviated: function() { + return i18n( + ['timerOption', this.get('time'), this.get('unit'), 'abbreviated'].join( + '_' + ) + ); + }, + }); + Whisper.ExpirationTimerOptions = new (Backbone.Collection.extend({ + model: TimerOption, + getName: function(seconds) { + if (!seconds) { + seconds = 0; + } + var o = this.findWhere({ seconds: seconds }); + if (o) { + return o.getName(); + } else { + return [seconds, 'seconds'].join(' '); + } + }, + getAbbreviated: function(seconds) { + if (!seconds) { + seconds = 0; + } + var o = this.findWhere({ seconds: seconds }); + if (o) { + return o.getAbbreviated(); + } else { + return [seconds, 's'].join(''); + } + }, + }))( + [ + [0, 'seconds'], + [5, 'seconds'], + [10, 'seconds'], + [30, 'seconds'], + [1, 'minute'], + [5, 'minutes'], + [30, 'minutes'], + [1, 'hour'], + [6, 'hours'], + [12, 'hours'], + [1, 'day'], + [1, 'week'], ].map(function(o) { var duration = moment.duration(o[0], o[1]); // 5, 'seconds' return { time: o[0], unit: o[1], - seconds: duration.asSeconds() + seconds: duration.asSeconds(), }; - })); - + }) + ); })(); diff --git a/js/focus_listener.js b/js/focus_listener.js index 04d4d33e3..c76c9d111 100644 --- a/js/focus_listener.js +++ b/js/focus_listener.js @@ -1,4 +1,4 @@ -(function () { +(function() { 'use strict'; var windowFocused = false; diff --git a/js/keychange_listener.js b/js/keychange_listener.js index 927adce9b..cca039d0a 100644 --- a/js/keychange_listener.js +++ b/js/keychange_listener.js @@ -2,27 +2,31 @@ * vim: ts=4:sw=4:expandtab */ -;(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.KeyChangeListener = { - init: function(signalProtocolStore) { - if (!(signalProtocolStore instanceof SignalProtocolStore)) { - throw new Error('KeyChangeListener requires a SignalProtocolStore'); - } + Whisper.KeyChangeListener = { + init: function(signalProtocolStore) { + if (!(signalProtocolStore instanceof SignalProtocolStore)) { + throw new Error('KeyChangeListener requires a SignalProtocolStore'); + } - signalProtocolStore.on('keychange', function(id) { - ConversationController.getOrCreateAndWait(id, 'private').then(function(conversation) { - conversation.addKeyChange(id); + signalProtocolStore.on('keychange', function(id) { + ConversationController.getOrCreateAndWait(id, 'private').then(function( + conversation + ) { + conversation.addKeyChange(id); - ConversationController.getAllGroupsInvolvingId(id).then(function(groups) { - _.forEach(groups, function(group) { - group.addKeyChange(id); - }); + ConversationController.getAllGroupsInvolvingId(id).then(function( + groups + ) { + _.forEach(groups, function(group) { + group.addKeyChange(id); }); }); }); - } - }; -}()); + }); + }, + }; +})(); diff --git a/js/libphonenumber-util.js b/js/libphonenumber-util.js index 4a57716b0..8e4f10274 100644 --- a/js/libphonenumber-util.js +++ b/js/libphonenumber-util.js @@ -1,8 +1,8 @@ /* * vim: ts=4:sw=4:expandtab */ -;(function() { - "use strict"; +(function() { + 'use strict'; /* * This file extends the libphonenumber object with a set of phonenumbery @@ -16,22 +16,22 @@ try { var parsedNumber = libphonenumber.parse(number); return libphonenumber.getRegionCodeForNumber(parsedNumber); - } catch(e) { - return "ZZ"; + } catch (e) { + return 'ZZ'; } }, splitCountryCode: function(number) { - var parsedNumber = libphonenumber.parse(number); - return { - country_code: parsedNumber.values_[1], - national_number: parsedNumber.values_[2] - }; + var parsedNumber = libphonenumber.parse(number); + return { + country_code: parsedNumber.values_[1], + national_number: parsedNumber.values_[2], + }; }, getCountryCode: function(regionCode) { var cc = libphonenumber.getCountryCodeForRegion(regionCode); - return (cc !== 0) ? cc : ""; + return cc !== 0 ? cc : ''; }, parseNumber: function(number, defaultRegionCode) { @@ -39,11 +39,14 @@ var parsedNumber = libphonenumber.parse(number, defaultRegionCode); return { - isValidNumber: libphonenumber.isValidNumber(parsedNumber), - regionCode: libphonenumber.getRegionCodeForNumber(parsedNumber), - countryCode: '' + parsedNumber.getCountryCode(), - nationalNumber: '' + parsedNumber.getNationalNumber(), - e164: libphonenumber.format(parsedNumber, libphonenumber.PhoneNumberFormat.E164) + isValidNumber: libphonenumber.isValidNumber(parsedNumber), + regionCode: libphonenumber.getRegionCodeForNumber(parsedNumber), + countryCode: '' + parsedNumber.getCountryCode(), + nationalNumber: '' + parsedNumber.getNationalNumber(), + e164: libphonenumber.format( + parsedNumber, + libphonenumber.PhoneNumberFormat.E164 + ), }; } catch (ex) { return { error: ex, isValidNumber: false }; @@ -52,244 +55,244 @@ getAllRegionCodes: function() { return { - "AD":"Andorra", - "AE":"United Arab Emirates", - "AF":"Afghanistan", - "AG":"Antigua and Barbuda", - "AI":"Anguilla", - "AL":"Albania", - "AM":"Armenia", - "AO":"Angola", - "AR":"Argentina", - "AS":"AmericanSamoa", - "AT":"Austria", - "AU":"Australia", - "AW":"Aruba", - "AX":"Åland Islands", - "AZ":"Azerbaijan", - "BA":"Bosnia and Herzegovina", - "BB":"Barbados", - "BD":"Bangladesh", - "BE":"Belgium", - "BF":"Burkina Faso", - "BG":"Bulgaria", - "BH":"Bahrain", - "BI":"Burundi", - "BJ":"Benin", - "BL":"Saint Barthélemy", - "BM":"Bermuda", - "BN":"Brunei Darussalam", - "BO":"Bolivia, Plurinational State of", - "BR":"Brazil", - "BS":"Bahamas", - "BT":"Bhutan", - "BW":"Botswana", - "BY":"Belarus", - "BZ":"Belize", - "CA":"Canada", - "CC":"Cocos (Keeling) Islands", - "CD":"Congo, The Democratic Republic of the", - "CF":"Central African Republic", - "CG":"Congo", - "CH":"Switzerland", - "CI":"Cote d'Ivoire", - "CK":"Cook Islands", - "CL":"Chile", - "CM":"Cameroon", - "CN":"China", - "CO":"Colombia", - "CR":"Costa Rica", - "CU":"Cuba", - "CV":"Cape Verde", - "CX":"Christmas Island", - "CY":"Cyprus", - "CZ":"Czech Republic", - "DE":"Germany", - "DJ":"Djibouti", - "DK":"Denmark", - "DM":"Dominica", - "DO":"Dominican Republic", - "DZ":"Algeria", - "EC":"Ecuador", - "EE":"Estonia", - "EG":"Egypt", - "ER":"Eritrea", - "ES":"Spain", - "ET":"Ethiopia", - "FI":"Finland", - "FJ":"Fiji", - "FK":"Falkland Islands (Malvinas)", - "FM":"Micronesia, Federated States of", - "FO":"Faroe Islands", - "FR":"France", - "GA":"Gabon", - "GB":"United Kingdom", - "GD":"Grenada", - "GE":"Georgia", - "GF":"French Guiana", - "GG":"Guernsey", - "GH":"Ghana", - "GI":"Gibraltar", - "GL":"Greenland", - "GM":"Gambia", - "GN":"Guinea", - "GP":"Guadeloupe", - "GQ":"Equatorial Guinea", - "GR":"Ελλάδα", - "GT":"Guatemala", - "GU":"Guam", - "GW":"Guinea-Bissau", - "GY":"Guyana", - "HK":"Hong Kong", - "HN":"Honduras", - "HR":"Croatia", - "HT":"Haiti", - "HU":"Magyarország", - "ID":"Indonesia", - "IE":"Ireland", - "IL":"Israel", - "IM":"Isle of Man", - "IN":"India", - "IO":"British Indian Ocean Territory", - "IQ":"Iraq", - "IR":"Iran, Islamic Republic of", - "IS":"Iceland", - "IT":"Italy", - "JE":"Jersey", - "JM":"Jamaica", - "JO":"Jordan", - "JP":"Japan", - "KE":"Kenya", - "KG":"Kyrgyzstan", - "KH":"Cambodia", - "KI":"Kiribati", - "KM":"Comoros", - "KN":"Saint Kitts and Nevis", - "KP":"Korea, Democratic People's Republic of", - "KR":"Korea, Republic of", - "KW":"Kuwait", - "KY":"Cayman Islands", - "KZ":"Kazakhstan", - "LA":"Lao People's Democratic Republic", - "LB":"Lebanon", - "LC":"Saint Lucia", - "LI":"Liechtenstein", - "LK":"Sri Lanka", - "LR":"Liberia", - "LS":"Lesotho", - "LT":"Lithuania", - "LU":"Luxembourg", - "LV":"Latvia", - "LY":"Libyan Arab Jamahiriya", - "MA":"Morocco", - "MC":"Monaco", - "MD":"Moldova, Republic of", - "ME":"Црна Гора", - "MF":"Saint Martin", - "MG":"Madagascar", - "MH":"Marshall Islands", - "MK":"Macedonia, The Former Yugoslav Republic of", - "ML":"Mali", - "MM":"Myanmar", - "MN":"Mongolia", - "MO":"Macao", - "MP":"Northern Mariana Islands", - "MQ":"Martinique", - "MR":"Mauritania", - "MS":"Montserrat", - "MT":"Malta", - "MU":"Mauritius", - "MV":"Maldives", - "MW":"Malawi", - "MX":"Mexico", - "MY":"Malaysia", - "MZ":"Mozambique", - "NA":"Namibia", - "NC":"New Caledonia", - "NE":"Niger", - "NF":"Norfolk Island", - "NG":"Nigeria", - "NI":"Nicaragua", - "NL":"Netherlands", - "NO":"Norway", - "NP":"Nepal", - "NR":"Nauru", - "NU":"Niue", - "NZ":"New Zealand", - "OM":"Oman", - "PA":"Panama", - "PE":"Peru", - "PF":"French Polynesia", - "PG":"Papua New Guinea", - "PH":"Philippines", - "PK":"Pakistan", - "PL":"Polska", - "PM":"Saint Pierre and Miquelon", - "PR":"Puerto Rico", - "PS":"Palestinian Territory, Occupied", - "PT":"Portugal", - "PW":"Palau", - "PY":"Paraguay", - "QA":"Qatar", - "RE":"Réunion", - "RO":"Romania", - "RS":"Србија", - "RU":"Russia", - "RW":"Rwanda", - "SA":"Saudi Arabia", - "SB":"Solomon Islands", - "SC":"Seychelles", - "SD":"Sudan", - "SE":"Sweden", - "SG":"Singapore", - "SH":"Saint Helena, Ascension and Tristan Da Cunha", - "SI":"Slovenia", - "SJ":"Svalbard and Jan Mayen", - "SK":"Slovakia", - "SL":"Sierra Leone", - "SM":"San Marino", - "SN":"Senegal", - "SO":"Somalia", - "SR":"Suriname", - "ST":"Sao Tome and Principe", - "SV":"El Salvador", - "SY":"Syrian Arab Republic", - "SZ":"Swaziland", - "TC":"Turks and Caicos Islands", - "TD":"Chad", - "TG":"Togo", - "TH":"Thailand", - "TJ":"Tajikistan", - "TK":"Tokelau", - "TL":"Timor-Leste", - "TM":"Turkmenistan", - "TN":"Tunisia", - "TO":"Tonga", - "TR":"Turkey", - "TT":"Trinidad and Tobago", - "TV":"Tuvalu", - "TW":"Taiwan, Province of China", - "TZ":"Tanzania, United Republic of", - "UA":"Ukraine", - "UG":"Uganda", - "US":"United States", - "UY":"Uruguay", - "UZ":"Uzbekistan", - "VA":"Holy See (Vatican City State)", - "VC":"Saint Vincent and the Grenadines", - "VE":"Venezuela", - "VG":"Virgin Islands, British", - "VI":"Virgin Islands, U.S.", - "VN":"Viet Nam", - "VU":"Vanuatu", - "WF":"Wallis and Futuna", - "WS":"Samoa", - "YE":"Yemen", - "YT":"Mayotte", - "ZA":"South Africa", - "ZM":"Zambia", - "ZW":"Zimbabwe" + AD: 'Andorra', + AE: 'United Arab Emirates', + AF: 'Afghanistan', + AG: 'Antigua and Barbuda', + AI: 'Anguilla', + AL: 'Albania', + AM: 'Armenia', + AO: 'Angola', + AR: 'Argentina', + AS: 'AmericanSamoa', + AT: 'Austria', + AU: 'Australia', + AW: 'Aruba', + AX: 'Åland Islands', + AZ: 'Azerbaijan', + BA: 'Bosnia and Herzegovina', + BB: 'Barbados', + BD: 'Bangladesh', + BE: 'Belgium', + BF: 'Burkina Faso', + BG: 'Bulgaria', + BH: 'Bahrain', + BI: 'Burundi', + BJ: 'Benin', + BL: 'Saint Barthélemy', + BM: 'Bermuda', + BN: 'Brunei Darussalam', + BO: 'Bolivia, Plurinational State of', + BR: 'Brazil', + BS: 'Bahamas', + BT: 'Bhutan', + BW: 'Botswana', + BY: 'Belarus', + BZ: 'Belize', + CA: 'Canada', + CC: 'Cocos (Keeling) Islands', + CD: 'Congo, The Democratic Republic of the', + CF: 'Central African Republic', + CG: 'Congo', + CH: 'Switzerland', + CI: "Cote d'Ivoire", + CK: 'Cook Islands', + CL: 'Chile', + CM: 'Cameroon', + CN: 'China', + CO: 'Colombia', + CR: 'Costa Rica', + CU: 'Cuba', + CV: 'Cape Verde', + CX: 'Christmas Island', + CY: 'Cyprus', + CZ: 'Czech Republic', + DE: 'Germany', + DJ: 'Djibouti', + DK: 'Denmark', + DM: 'Dominica', + DO: 'Dominican Republic', + DZ: 'Algeria', + EC: 'Ecuador', + EE: 'Estonia', + EG: 'Egypt', + ER: 'Eritrea', + ES: 'Spain', + ET: 'Ethiopia', + FI: 'Finland', + FJ: 'Fiji', + FK: 'Falkland Islands (Malvinas)', + FM: 'Micronesia, Federated States of', + FO: 'Faroe Islands', + FR: 'France', + GA: 'Gabon', + GB: 'United Kingdom', + GD: 'Grenada', + GE: 'Georgia', + GF: 'French Guiana', + GG: 'Guernsey', + GH: 'Ghana', + GI: 'Gibraltar', + GL: 'Greenland', + GM: 'Gambia', + GN: 'Guinea', + GP: 'Guadeloupe', + GQ: 'Equatorial Guinea', + GR: 'Ελλάδα', + GT: 'Guatemala', + GU: 'Guam', + GW: 'Guinea-Bissau', + GY: 'Guyana', + HK: 'Hong Kong', + HN: 'Honduras', + HR: 'Croatia', + HT: 'Haiti', + HU: 'Magyarország', + ID: 'Indonesia', + IE: 'Ireland', + IL: 'Israel', + IM: 'Isle of Man', + IN: 'India', + IO: 'British Indian Ocean Territory', + IQ: 'Iraq', + IR: 'Iran, Islamic Republic of', + IS: 'Iceland', + IT: 'Italy', + JE: 'Jersey', + JM: 'Jamaica', + JO: 'Jordan', + JP: 'Japan', + KE: 'Kenya', + KG: 'Kyrgyzstan', + KH: 'Cambodia', + KI: 'Kiribati', + KM: 'Comoros', + KN: 'Saint Kitts and Nevis', + KP: "Korea, Democratic People's Republic of", + KR: 'Korea, Republic of', + KW: 'Kuwait', + KY: 'Cayman Islands', + KZ: 'Kazakhstan', + LA: "Lao People's Democratic Republic", + LB: 'Lebanon', + LC: 'Saint Lucia', + LI: 'Liechtenstein', + LK: 'Sri Lanka', + LR: 'Liberia', + LS: 'Lesotho', + LT: 'Lithuania', + LU: 'Luxembourg', + LV: 'Latvia', + LY: 'Libyan Arab Jamahiriya', + MA: 'Morocco', + MC: 'Monaco', + MD: 'Moldova, Republic of', + ME: 'Црна Гора', + MF: 'Saint Martin', + MG: 'Madagascar', + MH: 'Marshall Islands', + MK: 'Macedonia, The Former Yugoslav Republic of', + ML: 'Mali', + MM: 'Myanmar', + MN: 'Mongolia', + MO: 'Macao', + MP: 'Northern Mariana Islands', + MQ: 'Martinique', + MR: 'Mauritania', + MS: 'Montserrat', + MT: 'Malta', + MU: 'Mauritius', + MV: 'Maldives', + MW: 'Malawi', + MX: 'Mexico', + MY: 'Malaysia', + MZ: 'Mozambique', + NA: 'Namibia', + NC: 'New Caledonia', + NE: 'Niger', + NF: 'Norfolk Island', + NG: 'Nigeria', + NI: 'Nicaragua', + NL: 'Netherlands', + NO: 'Norway', + NP: 'Nepal', + NR: 'Nauru', + NU: 'Niue', + NZ: 'New Zealand', + OM: 'Oman', + PA: 'Panama', + PE: 'Peru', + PF: 'French Polynesia', + PG: 'Papua New Guinea', + PH: 'Philippines', + PK: 'Pakistan', + PL: 'Polska', + PM: 'Saint Pierre and Miquelon', + PR: 'Puerto Rico', + PS: 'Palestinian Territory, Occupied', + PT: 'Portugal', + PW: 'Palau', + PY: 'Paraguay', + QA: 'Qatar', + RE: 'Réunion', + RO: 'Romania', + RS: 'Србија', + RU: 'Russia', + RW: 'Rwanda', + SA: 'Saudi Arabia', + SB: 'Solomon Islands', + SC: 'Seychelles', + SD: 'Sudan', + SE: 'Sweden', + SG: 'Singapore', + SH: 'Saint Helena, Ascension and Tristan Da Cunha', + SI: 'Slovenia', + SJ: 'Svalbard and Jan Mayen', + SK: 'Slovakia', + SL: 'Sierra Leone', + SM: 'San Marino', + SN: 'Senegal', + SO: 'Somalia', + SR: 'Suriname', + ST: 'Sao Tome and Principe', + SV: 'El Salvador', + SY: 'Syrian Arab Republic', + SZ: 'Swaziland', + TC: 'Turks and Caicos Islands', + TD: 'Chad', + TG: 'Togo', + TH: 'Thailand', + TJ: 'Tajikistan', + TK: 'Tokelau', + TL: 'Timor-Leste', + TM: 'Turkmenistan', + TN: 'Tunisia', + TO: 'Tonga', + TR: 'Turkey', + TT: 'Trinidad and Tobago', + TV: 'Tuvalu', + TW: 'Taiwan, Province of China', + TZ: 'Tanzania, United Republic of', + UA: 'Ukraine', + UG: 'Uganda', + US: 'United States', + UY: 'Uruguay', + UZ: 'Uzbekistan', + VA: 'Holy See (Vatican City State)', + VC: 'Saint Vincent and the Grenadines', + VE: 'Venezuela', + VG: 'Virgin Islands, British', + VI: 'Virgin Islands, U.S.', + VN: 'Viet Nam', + VU: 'Vanuatu', + WF: 'Wallis and Futuna', + WS: 'Samoa', + YE: 'Yemen', + YT: 'Mayotte', + ZA: 'South Africa', + ZM: 'Zambia', + ZW: 'Zimbabwe', }; - } // getAllRegionCodes + }, // getAllRegionCodes }; // libphonenumber.util })(); diff --git a/js/logging.js b/js/logging.js index 2538503b0..3c5b0d339 100644 --- a/js/logging.js +++ b/js/logging.js @@ -34,7 +34,7 @@ function log(...args) { console._log(...consoleArgs); // To avoid [Object object] in our log since console.log handles non-strings smoothly - const str = args.map((item) => { + const str = args.map(item => { if (typeof item !== 'string') { try { return JSON.stringify(item); @@ -55,7 +55,6 @@ if (window.console) { console.log = log; } - // The mechanics of preparing a log for publish function getHeader() { @@ -85,7 +84,7 @@ function format(entries) { } function fetch() { - return new Promise((resolve) => { + return new Promise(resolve => { ipc.send('fetch-log'); ipc.on('fetched-log', (event, text) => { @@ -103,14 +102,16 @@ const publish = debuglogs.upload; // Anyway, the default process.stdout stream goes to the command-line, not the devtools. const logger = bunyan.createLogger({ name: 'log', - streams: [{ - level: 'debug', - stream: { - write(entry) { - console._log(formatLine(JSON.parse(entry))); + streams: [ + { + level: 'debug', + stream: { + write(entry) { + console._log(formatLine(JSON.parse(entry))); + }, }, }, - }], + ], }); // The Bunyan API: https://github.com/trentm/node-bunyan#log-method-api @@ -137,6 +138,8 @@ window.onerror = (message, script, line, col, error) => { window.log.error(`Top-level unhandled error: ${errorInfo}`); }; -window.addEventListener('unhandledrejection', (rejectionEvent) => { - window.log.error(`Top-level unhandled promise rejection: ${rejectionEvent.reason}`); +window.addEventListener('unhandledrejection', rejectionEvent => { + window.log.error( + `Top-level unhandled promise rejection: ${rejectionEvent.reason}` + ); }); diff --git a/js/models/blockedNumbers.js b/js/models/blockedNumbers.js index 1eaa7af61..25421366f 100644 --- a/js/models/blockedNumbers.js +++ b/js/models/blockedNumbers.js @@ -1,29 +1,29 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - storage.isBlocked = function(number) { - var numbers = storage.get('blocked', []); +(function() { + 'use strict'; + storage.isBlocked = function(number) { + var numbers = storage.get('blocked', []); - return _.include(numbers, number); - }; - storage.addBlockedNumber = function(number) { - var numbers = storage.get('blocked', []); - if (_.include(numbers, number)) { - return; - } + return _.include(numbers, number); + }; + storage.addBlockedNumber = function(number) { + var numbers = storage.get('blocked', []); + if (_.include(numbers, number)) { + return; + } - console.log('adding', number, 'to blocked list'); - storage.put('blocked', numbers.concat(number)); - }; - storage.removeBlockedNumber = function(number) { - var numbers = storage.get('blocked', []); - if (!_.include(numbers, number)) { - return; - } + console.log('adding', number, 'to blocked list'); + storage.put('blocked', numbers.concat(number)); + }; + storage.removeBlockedNumber = function(number) { + var numbers = storage.get('blocked', []); + if (!_.include(numbers, number)) { + return; + } - console.log('removing', number, 'from blocked list'); - storage.put('blocked', _.without(numbers, number)); - }; + console.log('removing', number, 'from blocked list'); + storage.put('blocked', _.without(numbers, number)); + }; })(); diff --git a/js/models/conversations.js b/js/models/conversations.js index b714a5363..d4034969f 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -14,7 +14,7 @@ /* eslint-disable more/no-then */ // eslint-disable-next-line func-names -(function () { +(function() { 'use strict'; window.Whisper = window.Whisper || {}; @@ -124,27 +124,34 @@ }, safeGetVerified() { const promise = textsecure.storage.protocol.getVerified(this.id); - return promise.catch(() => textsecure.storage.protocol.VerifiedStatus.DEFAULT); + return promise.catch( + () => textsecure.storage.protocol.VerifiedStatus.DEFAULT + ); }, updateVerified() { if (this.isPrivate()) { - return Promise.all([ - this.safeGetVerified(), - this.initialPromise, - ]).then((results) => { - const trust = results[0]; - // we don't return here because we don't need to wait for this to finish - this.save({ verified: trust }); - }); + return Promise.all([this.safeGetVerified(), this.initialPromise]).then( + results => { + const trust = results[0]; + // we don't return here because we don't need to wait for this to finish + this.save({ verified: trust }); + } + ); } const promise = this.fetchContacts(); - return promise.then(() => Promise.all(this.contactCollection.map((contact) => { - if (!contact.isMe()) { - return contact.updateVerified(); - } - return Promise.resolve(); - }))).then(this.onMemberVerifiedChange.bind(this)); + return promise + .then(() => + Promise.all( + this.contactCollection.map(contact => { + if (!contact.isMe()) { + return contact.updateVerified(); + } + return Promise.resolve(); + }) + ) + ) + .then(this.onMemberVerifiedChange.bind(this)); }, setVerifiedDefault(options) { const { DEFAULT } = this.verifiedEnum; @@ -160,16 +167,19 @@ }, _setVerified(verified, providedOptions) { const options = providedOptions || {}; - _.defaults(options, { viaSyncMessage: false, viaContactSync: false, key: null }); + _.defaults(options, { + viaSyncMessage: false, + viaContactSync: false, + key: null, + }); - const { - VERIFIED, - UNVERIFIED, - } = this.verifiedEnum; + const { VERIFIED, UNVERIFIED } = this.verifiedEnum; if (!this.isPrivate()) { - throw new Error('You cannot verify a group conversation. ' + - 'You must verify individual contacts.'); + throw new Error( + 'You cannot verify a group conversation. ' + + 'You must verify individual contacts.' + ); } const beginningVerified = this.get('verified'); @@ -187,80 +197,83 @@ } let keychange; - return promise.then((updatedKey) => { - keychange = updatedKey; - return new Promise((resolve => this.save({ verified }).always(resolve))); - }).then(() => { - // Three situations result in a verification notice in the conversation: - // 1) The message came from an explicit verification in another client (not - // a contact sync) - // 2) The verification value received by the contact sync is different - // from what we have on record (and it's not a transition to UNVERIFIED) - // 3) Our local verification status is VERIFIED and it hasn't changed, - // but the key did change (Key1/VERIFIED to Key2/VERIFIED - but we don't - // want to show DEFAULT->DEFAULT or UNVERIFIED->UNVERIFIED) - if (!options.viaContactSync || - (beginningVerified !== verified && verified !== UNVERIFIED) || - (keychange && verified === VERIFIED)) { - return this.addVerifiedChange( - this.id, - verified === VERIFIED, - { local: !options.viaSyncMessage } + return promise + .then(updatedKey => { + keychange = updatedKey; + return new Promise(resolve => + this.save({ verified }).always(resolve) ); - } - if (!options.viaSyncMessage) { - return this.sendVerifySyncMessage(this.id, verified); - } - return Promise.resolve(); - }); + }) + .then(() => { + // Three situations result in a verification notice in the conversation: + // 1) The message came from an explicit verification in another client (not + // a contact sync) + // 2) The verification value received by the contact sync is different + // from what we have on record (and it's not a transition to UNVERIFIED) + // 3) Our local verification status is VERIFIED and it hasn't changed, + // but the key did change (Key1/VERIFIED to Key2/VERIFIED - but we don't + // want to show DEFAULT->DEFAULT or UNVERIFIED->UNVERIFIED) + if ( + !options.viaContactSync || + (beginningVerified !== verified && verified !== UNVERIFIED) || + (keychange && verified === VERIFIED) + ) { + return this.addVerifiedChange(this.id, verified === VERIFIED, { + local: !options.viaSyncMessage, + }); + } + if (!options.viaSyncMessage) { + return this.sendVerifySyncMessage(this.id, verified); + } + return Promise.resolve(); + }); }, sendVerifySyncMessage(number, state) { const promise = textsecure.storage.protocol.loadIdentityKey(number); - return promise.then(key => textsecure.messaging.syncVerification( - number, - state, - key - )); + return promise.then(key => + textsecure.messaging.syncVerification(number, state, key) + ); }, getIdentityKeys() { const lookup = {}; if (this.isPrivate()) { - return textsecure.storage.protocol.loadIdentityKey(this.id).then((key) => { - lookup[this.id] = key; - return lookup; - }).catch((error) => { - console.log( - 'getIdentityKeys error for conversation', - this.idForLogging(), - error && error.stack ? error.stack : error - ); - return lookup; - }); + return textsecure.storage.protocol + .loadIdentityKey(this.id) + .then(key => { + lookup[this.id] = key; + return lookup; + }) + .catch(error => { + console.log( + 'getIdentityKeys error for conversation', + this.idForLogging(), + error && error.stack ? error.stack : error + ); + return lookup; + }); } const promises = this.contactCollection.map(contact => textsecure.storage.protocol.loadIdentityKey(contact.id).then( - (key) => { + key => { lookup[contact.id] = key; }, - (error) => { + error => { console.log( 'getIdentityKeys error for group member', contact.idForLogging(), error && error.stack ? error.stack : error ); } - )); + ) + ); return Promise.all(promises).then(() => lookup); }, replay(error, message) { const replayable = new textsecure.ReplayableError(error); - return replayable.replay(message.attributes).catch((e) => { - console.log( - 'replay error:', - e && e.stack ? e.stack : e - ); + return replayable.replay(message.attributes).catch(e => { + console.log('replay error:', e && e.stack ? e.stack : e); }); }, decryptOldIncomingKeyErrors() { @@ -270,19 +283,25 @@ } console.log('decryptOldIncomingKeyErrors start for', this.idForLogging()); - const messages = this.messageCollection.filter((message) => { + const messages = this.messageCollection.filter(message => { const errors = message.get('errors'); if (!errors || !errors[0]) { return false; } - const error = _.find(errors, e => e.name === 'IncomingIdentityKeyError'); + const error = _.find( + errors, + e => e.name === 'IncomingIdentityKeyError' + ); return Boolean(error); }); const markComplete = () => { - console.log('decryptOldIncomingKeyErrors complete for', this.idForLogging()); - return new Promise((resolve) => { + console.log( + 'decryptOldIncomingKeyErrors complete for', + this.idForLogging() + ); + return new Promise(resolve => { this.save({ decryptedOldIncomingKeyErrors: true }).always(resolve); }); }; @@ -296,34 +315,44 @@ messages.length, 'messages to process' ); - const safeDelete = message => new Promise((resolve) => { - message.destroy().always(resolve); - }); + const safeDelete = message => + new Promise(resolve => { + message.destroy().always(resolve); + }); const promise = this.getIdentityKeys(); - return promise.then(lookup => Promise.all(_.map(messages, (message) => { - const source = message.get('source'); - const error = _.find( - message.get('errors'), - e => e.name === 'IncomingIdentityKeyError' - ); + return promise + .then(lookup => + Promise.all( + _.map(messages, message => { + const source = message.get('source'); + const error = _.find( + message.get('errors'), + e => e.name === 'IncomingIdentityKeyError' + ); - const key = lookup[source]; - if (!key) { - return Promise.resolve(); - } + const key = lookup[source]; + if (!key) { + return Promise.resolve(); + } - if (constantTimeEqualArrayBuffers(key, error.identityKey)) { - return this.replay(error, message).then(() => safeDelete(message)); - } + if (constantTimeEqualArrayBuffers(key, error.identityKey)) { + return this.replay(error, message).then(() => + safeDelete(message) + ); + } - return Promise.resolve(); - }))).catch((error) => { - console.log( - 'decryptOldIncomingKeyErrors error:', - error && error.stack ? error.stack : error - ); - }).then(markComplete); + return Promise.resolve(); + }) + ) + ) + .catch(error => { + console.log( + 'decryptOldIncomingKeyErrors error:', + error && error.stack ? error.stack : error + ); + }) + .then(markComplete); }, isVerified() { if (this.isPrivate()) { @@ -333,7 +362,7 @@ return false; } - return this.contactCollection.every((contact) => { + return this.contactCollection.every(contact => { if (contact.isMe()) { return true; } @@ -343,14 +372,16 @@ isUnverified() { if (this.isPrivate()) { const verified = this.get('verified'); - return verified !== this.verifiedEnum.VERIFIED && - verified !== this.verifiedEnum.DEFAULT; + return ( + verified !== this.verifiedEnum.VERIFIED && + verified !== this.verifiedEnum.DEFAULT + ); } if (!this.contactCollection.length) { return true; } - return this.contactCollection.any((contact) => { + return this.contactCollection.any(contact => { if (contact.isMe()) { return false; } @@ -363,23 +394,29 @@ ? new Backbone.Collection([this]) : new Backbone.Collection(); } - return new Backbone.Collection(this.contactCollection.filter((contact) => { - if (contact.isMe()) { - return false; - } - return contact.isUnverified(); - })); + return new Backbone.Collection( + this.contactCollection.filter(contact => { + if (contact.isMe()) { + return false; + } + return contact.isUnverified(); + }) + ); }, setApproved() { if (!this.isPrivate()) { - throw new Error('You cannot set a group conversation as trusted. ' + - 'You must set individual contacts as trusted.'); + throw new Error( + 'You cannot set a group conversation as trusted. ' + + 'You must set individual contacts as trusted.' + ); } return textsecure.storage.protocol.setApproval(this.id, true); }, safeIsUntrusted() { - return textsecure.storage.protocol.isUntrusted(this.id).catch(() => false); + return textsecure.storage.protocol + .isUntrusted(this.id) + .catch(() => false); }, isUntrusted() { if (this.isPrivate()) { @@ -389,18 +426,20 @@ return Promise.resolve(false); } - return Promise.all(this.contactCollection.map((contact) => { - if (contact.isMe()) { - return false; - } - return contact.safeIsUntrusted(); - })).then(results => _.any(results, result => result)); + return Promise.all( + this.contactCollection.map(contact => { + if (contact.isMe()) { + return false; + } + return contact.safeIsUntrusted(); + }) + ).then(results => _.any(results, result => result)); }, getUntrusted() { // This is a bit ugly because isUntrusted() is async. Could do the work to cache // it locally, but we really only need it for this call. if (this.isPrivate()) { - return this.isUntrusted().then((untrusted) => { + return this.isUntrusted().then(untrusted => { if (untrusted) { return new Backbone.Collection([this]); } @@ -408,20 +447,24 @@ return new Backbone.Collection(); }); } - return Promise.all(this.contactCollection.map((contact) => { - if (contact.isMe()) { - return [false, contact]; - } - return Promise.all([contact.isUntrusted(), contact]); - })).then((results) => { - const filtered = _.filter(results, (result) => { + return Promise.all( + this.contactCollection.map(contact => { + if (contact.isMe()) { + return [false, contact]; + } + return Promise.all([contact.isUntrusted(), contact]); + }) + ).then(results => { + const filtered = _.filter(results, result => { const untrusted = result[0]; return untrusted; }); - return new Backbone.Collection(_.map(filtered, (result) => { - const contact = result[1]; - return contact; - })); + return new Backbone.Collection( + _.map(filtered, result => { + const contact = result[1]; + return contact; + }) + ); }); }, onMemberVerifiedChange() { @@ -461,7 +504,9 @@ _.defaults(options, { local: true }); if (this.isMe()) { - console.log('refusing to add verified change advisory for our own number'); + console.log( + 'refusing to add verified change advisory for our own number' + ); return; } @@ -488,8 +533,8 @@ message.save().then(this.trigger.bind(this, 'newmessage', message)); if (this.isPrivate()) { - ConversationController.getAllGroupsInvolvingId(id).then((groups) => { - _.forEach(groups, (group) => { + ConversationController.getAllGroupsInvolvingId(id).then(groups => { + _.forEach(groups, group => { group.addVerifiedChange(id, verified, options); }); }); @@ -512,31 +557,36 @@ // Lastly, we don't send read syncs for any message marked read due to a read // sync. That's a notification explosion we don't need. - return this.queueJob(() => this.markRead( - message.get('received_at'), - { sendReadReceipts: false } - )); + return this.queueJob(() => + this.markRead(message.get('received_at'), { sendReadReceipts: false }) + ); }, getUnread() { const conversationId = this.id; const unreadMessages = new Whisper.MessageCollection(); - return new Promise((resolve => unreadMessages.fetch({ - index: { - // 'unread' index - name: 'unread', - lower: [conversationId], - upper: [conversationId, Number.MAX_VALUE], - }, - }).always(() => { - resolve(unreadMessages); - }))); + return new Promise(resolve => + unreadMessages + .fetch({ + index: { + // 'unread' index + name: 'unread', + lower: [conversationId], + upper: [conversationId, Number.MAX_VALUE], + }, + }) + .always(() => { + resolve(unreadMessages); + }) + ); }, validate(attributes) { const required = ['id', 'type']; const missing = _.filter(required, attr => !attributes[attr]); - if (missing.length) { return `Conversation must have ${missing}`; } + if (missing.length) { + return `Conversation must have ${missing}`; + } if (attributes.type !== 'private' && attributes.type !== 'group') { return `Invalid conversation type: ${attributes.type}`; @@ -572,7 +622,12 @@ const name = this.get('name'); if (typeof name === 'string') { tokens.push(name.toLowerCase()); - tokens = tokens.concat(name.trim().toLowerCase().split(/[\s\-_()+]+/)); + tokens = tokens.concat( + name + .trim() + .toLowerCase() + .split(/[\s\-_()+]+/) + ); } if (this.isPrivate()) { const regionCode = storage.get('regionCode'); @@ -633,7 +688,9 @@ type: contentType, }); - const thumbnail = Signal.Util.GoogleChrome.isImageTypeSupported(contentType) + const thumbnail = Signal.Util.GoogleChrome.isImageTypeSupported( + contentType + ) ? await Whisper.FileInputView.makeImageThumbnail(128, objectUrl) : await Whisper.FileInputView.makeVideoThumbnail(128, objectUrl); @@ -661,20 +718,22 @@ author: contact.id, id: quotedMessage.get('sent_at'), text: quotedMessage.get('body'), - attachments: await Promise.all((attachments || []).map(async (attachment) => { - const { contentType } = attachment; - const willMakeThumbnail = - Signal.Util.GoogleChrome.isImageTypeSupported(contentType) || - Signal.Util.GoogleChrome.isVideoTypeSupported(contentType); + attachments: await Promise.all( + (attachments || []).map(async attachment => { + const { contentType } = attachment; + const willMakeThumbnail = + Signal.Util.GoogleChrome.isImageTypeSupported(contentType) || + Signal.Util.GoogleChrome.isVideoTypeSupported(contentType); - return { - contentType, - fileName: attachment.fileName, - thumbnail: willMakeThumbnail - ? await this.makeThumbnailAttachment(attachment) - : null, - }; - })), + return { + contentType, + fileName: attachment.fileName, + thumbnail: willMakeThumbnail + ? await this.makeThumbnailAttachment(attachment) + : null, + }; + }) + ), }; }, @@ -721,7 +780,9 @@ case Message.GROUP: return textsecure.messaging.sendMessageToGroup; default: - throw new TypeError(`Invalid conversation type: '${conversationType}'`); + throw new TypeError( + `Invalid conversation type: '${conversationType}'` + ); } })(); @@ -730,17 +791,20 @@ profileKey = storage.get('profileKey'); } - const attachmentsWithData = - await Promise.all(messageWithSchema.attachments.map(loadAttachmentData)); - message.send(sendFunction( - this.get('id'), - body, - attachmentsWithData, - quote, - now, - this.get('expireTimer'), - profileKey - )); + const attachmentsWithData = await Promise.all( + messageWithSchema.attachments.map(loadAttachmentData) + ); + message.send( + sendFunction( + this.get('id'), + body, + attachmentsWithData, + quote, + now, + this.get('expireTimer'), + profileKey + ) + ); }); }, @@ -749,13 +813,16 @@ await collection.fetchConversation(this.id, 1); const lastMessage = collection.at(0); - const lastMessageUpdate = window.Signal.Types.Conversation.createLastMessageUpdate({ - currentLastMessageText: this.get('lastMessage') || null, - currentTimestamp: this.get('timestamp') || null, - lastMessage: lastMessage ? lastMessage.toJSON() : null, - lastMessageNotificationText: lastMessage - ? lastMessage.getNotificationText() : null, - }); + const lastMessageUpdate = window.Signal.Types.Conversation.createLastMessageUpdate( + { + currentLastMessageText: this.get('lastMessage') || null, + currentTimestamp: this.get('timestamp') || null, + lastMessage: lastMessage ? lastMessage.toJSON() : null, + lastMessageNotificationText: lastMessage + ? lastMessage.getNotificationText() + : null, + } + ); this.set(lastMessageUpdate); @@ -779,8 +846,10 @@ if (!expireTimer) { expireTimer = null; } - if (this.get('expireTimer') === expireTimer || - (!expireTimer && !this.get('expireTimer'))) { + if ( + this.get('expireTimer') === expireTimer || + (!expireTimer && !this.get('expireTimer')) + ) { return Promise.resolve(); } @@ -881,12 +950,14 @@ received_at: now, group_update: groupUpdate, }); - message.send(textsecure.messaging.updateGroup( - this.id, - this.get('name'), - this.get('avatar'), - this.get('members') - )); + message.send( + textsecure.messaging.updateGroup( + this.id, + this.get('name'), + this.get('avatar'), + this.get('members') + ) + ); }, leaveGroup() { @@ -909,25 +980,30 @@ _.defaults(options, { sendReadReceipts: true }); const conversationId = this.id; - Whisper.Notifications.remove(Whisper.Notifications.where({ - conversationId, - })); + Whisper.Notifications.remove( + Whisper.Notifications.where({ + conversationId, + }) + ); - return this.getUnread().then((providedUnreadMessages) => { + return this.getUnread().then(providedUnreadMessages => { let unreadMessages = providedUnreadMessages; const promises = []; - const oldUnread = unreadMessages.filter(message => - message.get('received_at') <= newestUnreadDate); + const oldUnread = unreadMessages.filter( + message => message.get('received_at') <= newestUnreadDate + ); - let read = _.map(oldUnread, (providedM) => { + let read = _.map(oldUnread, providedM => { let m = providedM; if (this.messageCollection.get(m.id)) { m = this.messageCollection.get(m.id); } else { - console.log('Marked a message as read in the database, but ' + - 'it was not in messageCollection.'); + console.log( + 'Marked a message as read in the database, but ' + + 'it was not in messageCollection.' + ); } promises.push(m.markRead()); const errors = m.get('errors'); @@ -962,7 +1038,9 @@ if (storage.get('read-receipt-setting')) { _.each(_.groupBy(read, 'sender'), (receipts, sender) => { const timestamps = _.map(receipts, 'timestamp'); - promises.push(textsecure.messaging.sendReadReceipts(sender, timestamps)); + promises.push( + textsecure.messaging.sendReadReceipts(sender, timestamps) + ); }); } } @@ -990,63 +1068,67 @@ getProfile(id) { if (!textsecure.messaging) { - const message = 'Conversation.getProfile: textsecure.messaging not available'; + const message = + 'Conversation.getProfile: textsecure.messaging not available'; return Promise.reject(new Error(message)); } - return textsecure.messaging.getProfile(id).then((profile) => { - const identityKey = dcodeIO.ByteBuffer.wrap( - profile.identityKey, - 'base64' - ).toArrayBuffer(); + return textsecure.messaging + .getProfile(id) + .then(profile => { + const identityKey = dcodeIO.ByteBuffer.wrap( + profile.identityKey, + 'base64' + ).toArrayBuffer(); - return textsecure.storage.protocol.saveIdentity( - `${id}.1`, - identityKey, - false - ).then((changed) => { - if (changed) { - // save identity will close all sessions except for .1, so we - // must close that one manually. - const address = new libsignal.SignalProtocolAddress(id, 1); - console.log('closing session for', address.toString()); - const sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, - address - ); - return sessionCipher.closeOpenSessionForDevice(); - } - return Promise.resolve(); - }).then(() => { - const c = ConversationController.get(id); - return Promise.all([ - c.setProfileName(profile.name), - c.setProfileAvatar(profile.avatar), - ]).then( - // success - () => new Promise((resolve, reject) => { - c.save().then(resolve, reject); - }), - // fail - (e) => { - if (e.name === 'ProfileDecryptError') { - // probably the profile key has changed. - console.log( - 'decryptProfile error:', - id, - profile, - e && e.stack ? e.stack : e + return textsecure.storage.protocol + .saveIdentity(`${id}.1`, identityKey, false) + .then(changed => { + if (changed) { + // save identity will close all sessions except for .1, so we + // must close that one manually. + const address = new libsignal.SignalProtocolAddress(id, 1); + console.log('closing session for', address.toString()); + const sessionCipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + address ); + return sessionCipher.closeOpenSessionForDevice(); } - } + return Promise.resolve(); + }) + .then(() => { + const c = ConversationController.get(id); + return Promise.all([ + c.setProfileName(profile.name), + c.setProfileAvatar(profile.avatar), + ]).then( + // success + () => + new Promise((resolve, reject) => { + c.save().then(resolve, reject); + }), + // fail + e => { + if (e.name === 'ProfileDecryptError') { + // probably the profile key has changed. + console.log( + 'decryptProfile error:', + id, + profile, + e && e.stack ? e.stack : e + ); + } + } + ); + }); + }) + .catch(error => { + console.log( + 'getProfile error:', + error && error.stack ? error.stack : error ); }); - }).catch((error) => { - console.log( - 'getProfile error:', - error && error.stack ? error.stack : error - ); - }); }, setProfileName(encryptedName) { const key = this.get('profileKey'); @@ -1056,16 +1138,21 @@ try { // decode - const data = dcodeIO.ByteBuffer.wrap(encryptedName, 'base64').toArrayBuffer(); + const data = dcodeIO.ByteBuffer.wrap( + encryptedName, + 'base64' + ).toArrayBuffer(); // decrypt - return textsecure.crypto.decryptProfileName(data, key).then((decrypted) => { - // encode - const name = dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'); + return textsecure.crypto + .decryptProfileName(data, key) + .then(decrypted => { + // encode + const name = dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'); - // set - this.set({ profileName: name }); - }); + // set + this.set({ profileName: name }); + }); } catch (e) { return Promise.reject(e); } @@ -1075,13 +1162,13 @@ return Promise.resolve(); } - return textsecure.messaging.getAvatar(avatarPath).then((avatar) => { + return textsecure.messaging.getAvatar(avatarPath).then(avatar => { const key = this.get('profileKey'); if (!key) { return Promise.resolve(); } // decrypt - return textsecure.crypto.decryptProfile(avatar, key).then((decrypted) => { + return textsecure.crypto.decryptProfile(avatar, key).then(decrypted => { // set this.set({ profileAvatar: { @@ -1125,9 +1212,11 @@ const first = attachments[0]; const { thumbnail, contentType } = first; - return thumbnail || + return ( + thumbnail || Signal.Util.GoogleChrome.isImageTypeSupported(contentType) || - Signal.Util.GoogleChrome.isVideoTypeSupported(contentType); + Signal.Util.GoogleChrome.isVideoTypeSupported(contentType) + ); }, forceRender(message) { message.trigger('change', message); @@ -1163,14 +1252,18 @@ return false; } - if (!Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) && - !Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType)) { + if ( + !Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) && + !Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType) + ) { return false; } const collection = new Whisper.MessageCollection(); await collection.fetchSentAt(id); - const queryMessage = collection.find(m => this.doesMessageMatch(id, author, m)); + const queryMessage = collection.find(m => + this.doesMessageMatch(id, author, m) + ); if (!queryMessage) { return false; @@ -1206,8 +1299,10 @@ return; } - if (!Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) && - !Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType)) { + if ( + !Signal.Util.GoogleChrome.isImageTypeSupported(first.contentType) && + !Signal.Util.GoogleChrome.isVideoTypeSupported(first.contentType) + ) { return; } @@ -1267,7 +1362,7 @@ async processQuotes(messages) { const lookup = this.makeMessagesLookup(messages); - const promises = messages.map(async (message) => { + const promises = messages.map(async message => { const { quote } = message.attributes; if (!quote) { return; @@ -1350,11 +1445,16 @@ } const members = this.get('members') || []; const promises = members.map(number => - ConversationController.getOrCreateAndWait(number, 'private')); + ConversationController.getOrCreateAndWait(number, 'private') + ); - return Promise.all(promises).then((contacts) => { - _.forEach(contacts, (contact) => { - this.listenTo(contact, 'change:verified', this.onMemberVerifiedChange); + return Promise.all(promises).then(contacts => { + _.forEach(contacts, contact => { + this.listenTo( + contact, + 'change:verified', + this.onMemberVerifiedChange + ); }); this.contactCollection.reset(contacts); @@ -1362,25 +1462,27 @@ }, destroyMessages() { - this.messageCollection.fetch({ - index: { - // 'conversation' index on [conversationId, received_at] - name: 'conversation', - lower: [this.id], - upper: [this.id, Number.MAX_VALUE], - }, - }).then(() => { - const { models } = this.messageCollection; - this.messageCollection.reset([]); - _.each(models, (message) => { - message.destroy(); + this.messageCollection + .fetch({ + index: { + // 'conversation' index on [conversationId, received_at] + name: 'conversation', + lower: [this.id], + upper: [this.id, Number.MAX_VALUE], + }, + }) + .then(() => { + const { models } = this.messageCollection; + this.messageCollection.reset([]); + _.each(models, message => { + message.destroy(); + }); + this.save({ + lastMessage: null, + timestamp: null, + active_at: null, + }); }); - this.save({ - lastMessage: null, - timestamp: null, - active_at: null, - }); - }); }, getName() { @@ -1460,10 +1562,9 @@ this.revokeAvatarUrl(); const avatar = this.get('avatar') || this.get('profileAvatar'); if (avatar) { - this.avatarUrl = URL.createObjectURL(new Blob( - [avatar.data], - { type: avatar.contentType } - )); + this.avatarUrl = URL.createObjectURL( + new Blob([avatar.data], { type: avatar.contentType }) + ); } else { this.avatarUrl = null; } @@ -1507,7 +1608,7 @@ }, getNotificationIcon() { - return new Promise((resolve) => { + return new Promise(resolve => { const avatar = this.getAvatar(); if (avatar.url) { resolve(avatar.url); @@ -1523,8 +1624,11 @@ } const conversationId = this.id; - return ConversationController.getOrCreateAndWait(message.get('source'), 'private') - .then(sender => sender.getNotificationIcon().then((iconUrl) => { + return ConversationController.getOrCreateAndWait( + message.get('source'), + 'private' + ).then(sender => + sender.getNotificationIcon().then(iconUrl => { console.log('adding notification'); Whisper.Notifications.add({ title: sender.getTitle(), @@ -1534,7 +1638,8 @@ conversationId, messageId: message.id, }); - })); + }) + ); }, hashCode() { if (this.hash === undefined) { @@ -1545,7 +1650,7 @@ let hash = 0; for (let i = 0; i < string.length; i += 1) { // eslint-disable-next-line no-bitwise - hash = ((hash << 5) - hash) + string.charCodeAt(i); + hash = (hash << 5) - hash + string.charCodeAt(i); // eslint-disable-next-line no-bitwise hash &= hash; // Convert to 32bit integer } @@ -1566,9 +1671,17 @@ }, destroyAll() { - return Promise.all(this.models.map(m => new Promise((resolve, reject) => { - m.destroy().then(resolve).fail(reject); - }))); + return Promise.all( + this.models.map( + m => + new Promise((resolve, reject) => { + m + .destroy() + .then(resolve) + .fail(reject); + }) + ) + ); }, search(providedQuery) { @@ -1578,7 +1691,7 @@ const lastCharCode = query.charCodeAt(query.length - 1); const nextChar = String.fromCharCode(lastCharCode + 1); const upper = query.slice(0, -1) + nextChar; - return new Promise((resolve) => { + return new Promise(resolve => { this.fetch({ index: { name: 'search', // 'search' index on tokens array @@ -1593,7 +1706,7 @@ }, fetchAlphabetical() { - return new Promise((resolve) => { + return new Promise(resolve => { this.fetch({ index: { name: 'search', // 'search' index on tokens array @@ -1604,7 +1717,7 @@ }, fetchGroups(number) { - return new Promise((resolve) => { + return new Promise(resolve => { this.fetch({ index: { name: 'group', @@ -1623,7 +1736,7 @@ storeName: 'conversations', model: Whisper.Conversation, fetchGroups(number) { - return new Promise((resolve) => { + return new Promise(resolve => { this.fetch({ index: { name: 'group', @@ -1633,4 +1746,4 @@ }); }, }); -}()); +})(); diff --git a/js/models/messages.js b/js/models/messages.js index 06652a43e..9b1d69a3a 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -9,7 +9,7 @@ /* eslint-disable more/no-then */ // eslint-disable-next-line func-names -(function () { +(function() { 'use strict'; window.Whisper = window.Whisper || {}; @@ -32,10 +32,13 @@ this.on('unload', this.unload); this.setToExpire(); - this.VOICE_FLAG = textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE; + this.VOICE_FLAG = + textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE; }, idForLogging() { - return `${this.get('source')}.${this.get('sourceDevice')} ${this.get('sent_at')}`; + return `${this.get('source')}.${this.get('sourceDevice')} ${this.get( + 'sent_at' + )}`; }, defaults() { return { @@ -56,12 +59,13 @@ return !!(this.get('flags') & flag); }, isExpirationTimerUpdate() { - const flag = textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; + const flag = + textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; // eslint-disable-next-line no-bitwise return !!(this.get('flags') & flag); }, isGroupUpdate() { - return !!(this.get('group_update')); + return !!this.get('group_update'); }, isIncoming() { return this.get('type') === 'incoming'; @@ -79,14 +83,14 @@ if (options.parse === void 0) options.parse = true; const model = this; const success = options.success; - options.success = function (resp) { + options.success = function(resp) { model.attributes = {}; // this is the only changed line if (!model.set(model.parse(resp, options), options)) return false; if (success) success(model, resp, options); model.trigger('sync', model, resp, options); }; const error = options.error; - options.error = function (resp) { + options.error = function(resp) { if (error) error(model, resp, options); model.trigger('error', model, resp, options); }; @@ -116,7 +120,10 @@ messages.push(i18n('titleIsNow', groupUpdate.name)); } if (groupUpdate.joined && groupUpdate.joined.length) { - const names = _.map(groupUpdate.joined, this.getNameForNumber.bind(this)); + const names = _.map( + groupUpdate.joined, + this.getNameForNumber.bind(this) + ); if (names.length > 1) { messages.push(i18n('multipleJoinedTheGroup', names.join(', '))); } else { @@ -186,7 +193,7 @@ } const quote = this.get('quote'); const attachments = (quote && quote.attachments) || []; - attachments.forEach((attachment) => { + attachments.forEach(attachment => { if (attachment.thumbnail && attachment.thumbnail.objectUrl) { URL.revokeObjectURL(attachment.thumbnail.objectUrl); // eslint-disable-next-line no-param-reassign @@ -235,8 +242,8 @@ const thumbnailWithObjectUrl = !objectUrl ? null : Object.assign({}, attachment.thumbnail || {}, { - objectUrl, - }); + objectUrl, + }); return Object.assign({}, attachment, { // eslint-disable-next-line no-bitwise @@ -269,7 +276,8 @@ return { attachments: (quote.attachments || []).map(attachment => - this.processAttachment(attachment, objectUrl)), + this.processAttachment(attachment, objectUrl) + ), authorColor, authorProfileName, authorTitle, @@ -342,59 +350,63 @@ send(promise) { this.trigger('pending'); - return promise.then((result) => { - const now = Date.now(); - this.trigger('done'); - if (result.dataMessage) { - this.set({ dataMessage: result.dataMessage }); - } - const sentTo = this.get('sent_to') || []; - this.save({ - sent_to: _.union(sentTo, result.successfulNumbers), - sent: true, - expirationStartTimestamp: now, - }); - this.sendSyncMessage(); - }).catch((result) => { - const now = Date.now(); - this.trigger('done'); - if (result.dataMessage) { - this.set({ dataMessage: result.dataMessage }); - } - - let promises = []; - - if (result instanceof Error) { - this.saveErrors(result); - if (result.name === 'SignedPreKeyRotationError') { - promises.push(getAccountManager().rotateSignedPreKey()); - } else if (result.name === 'OutgoingIdentityKeyError') { - const c = ConversationController.get(result.number); - promises.push(c.getProfiles()); + return promise + .then(result => { + const now = Date.now(); + this.trigger('done'); + if (result.dataMessage) { + this.set({ dataMessage: result.dataMessage }); } - } else { - this.saveErrors(result.errors); - if (result.successfulNumbers.length > 0) { - const sentTo = this.get('sent_to') || []; - this.set({ - sent_to: _.union(sentTo, result.successfulNumbers), - sent: true, - expirationStartTimestamp: now, - }); - promises.push(this.sendSyncMessage()); + const sentTo = this.get('sent_to') || []; + this.save({ + sent_to: _.union(sentTo, result.successfulNumbers), + sent: true, + expirationStartTimestamp: now, + }); + this.sendSyncMessage(); + }) + .catch(result => { + const now = Date.now(); + this.trigger('done'); + if (result.dataMessage) { + this.set({ dataMessage: result.dataMessage }); } - promises = promises.concat(_.map(result.errors, (error) => { - if (error.name === 'OutgoingIdentityKeyError') { - const c = ConversationController.get(error.number); + + let promises = []; + + if (result instanceof Error) { + this.saveErrors(result); + if (result.name === 'SignedPreKeyRotationError') { + promises.push(getAccountManager().rotateSignedPreKey()); + } else if (result.name === 'OutgoingIdentityKeyError') { + const c = ConversationController.get(result.number); promises.push(c.getProfiles()); } - })); - } + } else { + this.saveErrors(result.errors); + if (result.successfulNumbers.length > 0) { + const sentTo = this.get('sent_to') || []; + this.set({ + sent_to: _.union(sentTo, result.successfulNumbers), + sent: true, + expirationStartTimestamp: now, + }); + promises.push(this.sendSyncMessage()); + } + promises = promises.concat( + _.map(result.errors, error => { + if (error.name === 'OutgoingIdentityKeyError') { + const c = ConversationController.get(error.number); + promises.push(c.getProfiles()); + } + }) + ); + } - return Promise.all(promises).then(() => { - this.trigger('send-error', this.get('errors')); + return Promise.all(promises).then(() => { + this.trigger('send-error', this.get('errors')); + }); }); - }); }, someRecipientsFailed() { @@ -423,14 +435,16 @@ if (this.get('synced') || !dataMessage) { return Promise.resolve(); } - return textsecure.messaging.sendSyncMessage( - dataMessage, - this.get('sent_at'), - this.get('destination'), - this.get('expirationStartTimestamp') - ).then(() => { - this.save({ synced: true, dataMessage: null }); - }); + return textsecure.messaging + .sendSyncMessage( + dataMessage, + this.get('sent_at'), + this.get('destination'), + this.get('expirationStartTimestamp') + ) + .then(() => { + this.save({ synced: true, dataMessage: null }); + }); }); }, @@ -440,17 +454,19 @@ if (!(errors instanceof Array)) { errors = [errors]; } - errors.forEach((e) => { + errors.forEach(e => { console.log( 'Message.saveErrors:', e && e.reason ? e.reason : null, e && e.stack ? e.stack : e ); }); - errors = errors.map((e) => { - if (e.constructor === Error || - e.constructor === TypeError || - e.constructor === ReferenceError) { + errors = errors.map(e => { + if ( + e.constructor === Error || + e.constructor === TypeError || + e.constructor === ReferenceError + ) { return _.pick(e, 'name', 'message', 'code', 'number', 'reason'); } return e; @@ -463,32 +479,36 @@ hasNetworkError() { const error = _.find( this.get('errors'), - e => (e.name === 'MessageError' || - e.name === 'OutgoingMessageError' || - e.name === 'SendMessageNetworkError' || - e.name === 'SignedPreKeyRotationError') + e => + e.name === 'MessageError' || + e.name === 'OutgoingMessageError' || + e.name === 'SendMessageNetworkError' || + e.name === 'SignedPreKeyRotationError' ); return !!error; }, removeOutgoingErrors(number) { const errors = _.partition( this.get('errors'), - e => e.number === number && - (e.name === 'MessageError' || - e.name === 'OutgoingMessageError' || - e.name === 'SendMessageNetworkError' || - e.name === 'SignedPreKeyRotationError' || - e.name === 'OutgoingIdentityKeyError') + e => + e.number === number && + (e.name === 'MessageError' || + e.name === 'OutgoingMessageError' || + e.name === 'SendMessageNetworkError' || + e.name === 'SignedPreKeyRotationError' || + e.name === 'OutgoingIdentityKeyError') ); this.set({ errors: errors[1] }); return errors[0][0]; }, isReplayableError(e) { - return (e.name === 'MessageError' || - e.name === 'OutgoingMessageError' || - e.name === 'SendMessageNetworkError' || - e.name === 'SignedPreKeyRotationError' || - e.name === 'OutgoingIdentityKeyError'); + return ( + e.name === 'MessageError' || + e.name === 'OutgoingMessageError' || + e.name === 'SendMessageNetworkError' || + e.name === 'SignedPreKeyRotationError' || + e.name === 'OutgoingIdentityKeyError' + ); }, resend(number) { const error = this.removeOutgoingErrors(number); @@ -513,236 +533,280 @@ const GROUP_TYPES = textsecure.protobuf.GroupContext.Type; const conversation = ConversationController.get(conversationId); - return conversation.queueJob(() => new Promise((resolve) => { - const now = new Date().getTime(); - let attributes = { type: 'private' }; - if (dataMessage.group) { - let groupUpdate = null; - attributes = { - type: 'group', - groupId: dataMessage.group.id, - }; - if (dataMessage.group.type === GROUP_TYPES.UPDATE) { - attributes = { - type: 'group', - groupId: dataMessage.group.id, - name: dataMessage.group.name, - avatar: dataMessage.group.avatar, - members: _.union(dataMessage.group.members, conversation.get('members')), - }; - groupUpdate = conversation.changedAttributes(_.pick( - dataMessage.group, - 'name', - 'avatar' - )) || {}; - const difference = _.difference( - attributes.members, - conversation.get('members') - ); - if (difference.length > 0) { - groupUpdate.joined = difference; + return conversation.queueJob( + () => + new Promise(resolve => { + const now = new Date().getTime(); + let attributes = { type: 'private' }; + if (dataMessage.group) { + let groupUpdate = null; + attributes = { + type: 'group', + groupId: dataMessage.group.id, + }; + if (dataMessage.group.type === GROUP_TYPES.UPDATE) { + attributes = { + type: 'group', + groupId: dataMessage.group.id, + name: dataMessage.group.name, + avatar: dataMessage.group.avatar, + members: _.union( + dataMessage.group.members, + conversation.get('members') + ), + }; + groupUpdate = + conversation.changedAttributes( + _.pick(dataMessage.group, 'name', 'avatar') + ) || {}; + const difference = _.difference( + attributes.members, + conversation.get('members') + ); + if (difference.length > 0) { + groupUpdate.joined = difference; + } + if (conversation.get('left')) { + console.log('re-added to a left group'); + attributes.left = false; + } + } else if (dataMessage.group.type === GROUP_TYPES.QUIT) { + if (source === textsecure.storage.user.getNumber()) { + attributes.left = true; + groupUpdate = { left: 'You' }; + } else { + groupUpdate = { left: source }; + } + attributes.members = _.without( + conversation.get('members'), + source + ); + } + + if (groupUpdate !== null) { + message.set({ group_update: groupUpdate }); + } } - if (conversation.get('left')) { - console.log('re-added to a left group'); - attributes.left = false; - } - } else if (dataMessage.group.type === GROUP_TYPES.QUIT) { - if (source === textsecure.storage.user.getNumber()) { - attributes.left = true; - groupUpdate = { left: 'You' }; - } else { - groupUpdate = { left: source }; - } - attributes.members = _.without(conversation.get('members'), source); - } - - if (groupUpdate !== null) { - message.set({ group_update: groupUpdate }); - } - } - message.set({ - attachments: dataMessage.attachments, - body: dataMessage.body, - conversationId: conversation.id, - decrypted_at: now, - errors: [], - flags: dataMessage.flags, - hasAttachments: dataMessage.hasAttachments, - hasFileAttachments: dataMessage.hasFileAttachments, - hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments, - quote: dataMessage.quote, - schemaVersion: dataMessage.schemaVersion, - }); - if (type === 'outgoing') { - const receipts = Whisper.DeliveryReceipts.forMessage(conversation, message); - receipts.forEach(() => message.set({ - delivered: (message.get('delivered') || 0) + 1, - })); - } - attributes.active_at = now; - conversation.set(attributes); - - if (message.isExpirationTimerUpdate()) { - message.set({ - expirationTimerUpdate: { - source, - expireTimer: dataMessage.expireTimer, - }, - }); - conversation.set({ expireTimer: dataMessage.expireTimer }); - } else if (dataMessage.expireTimer) { - message.set({ expireTimer: dataMessage.expireTimer }); - } - - // NOTE: Remove once the above uses - // `Conversation::updateExpirationTimer`: - const { expireTimer } = dataMessage; - const shouldLogExpireTimerChange = - message.isExpirationTimerUpdate() || expireTimer; - if (shouldLogExpireTimerChange) { - console.log( - 'Updating expireTimer for conversation', - conversation.idForLogging(), - 'to', - expireTimer, - 'via `handleDataMessage`' - ); - } - - if (!message.isEndSession() && !message.isGroupUpdate()) { - if (dataMessage.expireTimer) { - if (dataMessage.expireTimer !== conversation.get('expireTimer')) { - conversation.updateExpirationTimer( - dataMessage.expireTimer, source, - message.get('received_at') + message.set({ + attachments: dataMessage.attachments, + body: dataMessage.body, + conversationId: conversation.id, + decrypted_at: now, + errors: [], + flags: dataMessage.flags, + hasAttachments: dataMessage.hasAttachments, + hasFileAttachments: dataMessage.hasFileAttachments, + hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments, + quote: dataMessage.quote, + schemaVersion: dataMessage.schemaVersion, + }); + if (type === 'outgoing') { + const receipts = Whisper.DeliveryReceipts.forMessage( + conversation, + message + ); + receipts.forEach(() => + message.set({ + delivered: (message.get('delivered') || 0) + 1, + }) ); } - } else if (conversation.get('expireTimer')) { - conversation.updateExpirationTimer( - null, source, - message.get('received_at') - ); - } - } - if (type === 'incoming') { - const readSync = Whisper.ReadSyncs.forMessage(message); - if (readSync) { - if (message.get('expireTimer') && !message.get('expirationStartTimestamp')) { - message.set('expirationStartTimestamp', readSync.get('read_at')); + attributes.active_at = now; + conversation.set(attributes); + + if (message.isExpirationTimerUpdate()) { + message.set({ + expirationTimerUpdate: { + source, + expireTimer: dataMessage.expireTimer, + }, + }); + conversation.set({ expireTimer: dataMessage.expireTimer }); + } else if (dataMessage.expireTimer) { + message.set({ expireTimer: dataMessage.expireTimer }); } - } - if (readSync || message.isExpirationTimerUpdate()) { - message.unset('unread'); - // This is primarily to allow the conversation to mark all older messages as - // read, as is done when we receive a read sync for a message we already - // know about. - Whisper.ReadSyncs.notifyConversation(message); - } else { - conversation.set('unreadCount', conversation.get('unreadCount') + 1); - } - } - if (type === 'outgoing') { - const reads = Whisper.ReadReceipts.forMessage(conversation, message); - if (reads.length) { - const readBy = reads.map(receipt => receipt.get('reader')); - message.set({ - read_by: _.union(message.get('read_by'), readBy), - }); - } - - message.set({ recipients: conversation.getRecipients() }); - } - - const conversationTimestamp = conversation.get('timestamp'); - if (!conversationTimestamp || message.get('sent_at') > conversationTimestamp) { - conversation.set({ - lastMessage: message.getNotificationText(), - timestamp: message.get('sent_at'), - }); - } - - if (dataMessage.profileKey) { - const profileKey = dataMessage.profileKey.toArrayBuffer(); - if (source === textsecure.storage.user.getNumber()) { - conversation.set({ profileSharing: true }); - } else if (conversation.isPrivate()) { - conversation.set({ profileKey }); - } else { - ConversationController.getOrCreateAndWait( - source, - 'private' - ).then((sender) => { - sender.setProfileKey(profileKey); - }); - } - } - - const handleError = (error) => { - const errorForLog = error && error.stack ? error.stack : error; - console.log('handleDataMessage', message.idForLogging(), 'error:', errorForLog); - return resolve(); - }; - - message.save().then(() => { - conversation.save().then(() => { - try { - conversation.trigger('newmessage', message); - } catch (e) { - return handleError(e); + // NOTE: Remove once the above uses + // `Conversation::updateExpirationTimer`: + const { expireTimer } = dataMessage; + const shouldLogExpireTimerChange = + message.isExpirationTimerUpdate() || expireTimer; + if (shouldLogExpireTimerChange) { + console.log( + 'Updating expireTimer for conversation', + conversation.idForLogging(), + 'to', + expireTimer, + 'via `handleDataMessage`' + ); } - // We fetch() here because, between the message.save() above and the previous - // line's trigger() call, we might have marked all messages unread in the - // database. This message might already be read! - const previousUnread = message.get('unread'); - return message.fetch().then(() => { - try { - if (previousUnread !== message.get('unread')) { - console.log('Caught race condition on new message read state! ' + - 'Manually starting timers.'); - // We call markRead() even though the message is already marked read - // because we need to start expiration timers, etc. - message.markRead(); - } - if (message.get('unread')) { - return conversation.notify(message).then(() => { - confirm(); - return resolve(); - }, handleError); + if (!message.isEndSession() && !message.isGroupUpdate()) { + if (dataMessage.expireTimer) { + if ( + dataMessage.expireTimer !== conversation.get('expireTimer') + ) { + conversation.updateExpirationTimer( + dataMessage.expireTimer, + source, + message.get('received_at') + ); } - - confirm(); - return resolve(); - } catch (e) { - return handleError(e); - } - }, () => { - try { - console.log( - 'handleDataMessage: Message', - message.idForLogging(), - 'was deleted' + } else if (conversation.get('expireTimer')) { + conversation.updateExpirationTimer( + null, + source, + message.get('received_at') ); - - confirm(); - return resolve(); - } catch (e) { - return handleError(e); } - }); - }, handleError); - }, handleError); - })); + } + if (type === 'incoming') { + const readSync = Whisper.ReadSyncs.forMessage(message); + if (readSync) { + if ( + message.get('expireTimer') && + !message.get('expirationStartTimestamp') + ) { + message.set( + 'expirationStartTimestamp', + readSync.get('read_at') + ); + } + } + if (readSync || message.isExpirationTimerUpdate()) { + message.unset('unread'); + // This is primarily to allow the conversation to mark all older messages as + // read, as is done when we receive a read sync for a message we already + // know about. + Whisper.ReadSyncs.notifyConversation(message); + } else { + conversation.set( + 'unreadCount', + conversation.get('unreadCount') + 1 + ); + } + } + + if (type === 'outgoing') { + const reads = Whisper.ReadReceipts.forMessage( + conversation, + message + ); + if (reads.length) { + const readBy = reads.map(receipt => receipt.get('reader')); + message.set({ + read_by: _.union(message.get('read_by'), readBy), + }); + } + + message.set({ recipients: conversation.getRecipients() }); + } + + const conversationTimestamp = conversation.get('timestamp'); + if ( + !conversationTimestamp || + message.get('sent_at') > conversationTimestamp + ) { + conversation.set({ + lastMessage: message.getNotificationText(), + timestamp: message.get('sent_at'), + }); + } + + if (dataMessage.profileKey) { + const profileKey = dataMessage.profileKey.toArrayBuffer(); + if (source === textsecure.storage.user.getNumber()) { + conversation.set({ profileSharing: true }); + } else if (conversation.isPrivate()) { + conversation.set({ profileKey }); + } else { + ConversationController.getOrCreateAndWait( + source, + 'private' + ).then(sender => { + sender.setProfileKey(profileKey); + }); + } + } + + const handleError = error => { + const errorForLog = error && error.stack ? error.stack : error; + console.log( + 'handleDataMessage', + message.idForLogging(), + 'error:', + errorForLog + ); + return resolve(); + }; + + message.save().then(() => { + conversation.save().then(() => { + try { + conversation.trigger('newmessage', message); + } catch (e) { + return handleError(e); + } + // We fetch() here because, between the message.save() above and the previous + // line's trigger() call, we might have marked all messages unread in the + // database. This message might already be read! + const previousUnread = message.get('unread'); + return message.fetch().then( + () => { + try { + if (previousUnread !== message.get('unread')) { + console.log( + 'Caught race condition on new message read state! ' + + 'Manually starting timers.' + ); + // We call markRead() even though the message is already marked read + // because we need to start expiration timers, etc. + message.markRead(); + } + + if (message.get('unread')) { + return conversation.notify(message).then(() => { + confirm(); + return resolve(); + }, handleError); + } + + confirm(); + return resolve(); + } catch (e) { + return handleError(e); + } + }, + () => { + try { + console.log( + 'handleDataMessage: Message', + message.idForLogging(), + 'was deleted' + ); + + confirm(); + return resolve(); + } catch (e) { + return handleError(e); + } + } + ); + }, handleError); + }, handleError); + }) + ); }, markRead(readAt) { this.unset('unread'); if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) { this.set('expirationStartTimestamp', readAt || Date.now()); } - Whisper.Notifications.remove(Whisper.Notifications.where({ - messageId: this.id, - })); + Whisper.Notifications.remove( + Whisper.Notifications.where({ + messageId: this.id, + }) + ); return new Promise((resolve, reject) => { this.save().then(resolve, reject); }); @@ -760,7 +824,7 @@ const now = Date.now(); const start = this.get('expirationStartTimestamp'); const delta = this.get('expireTimer') * 1000; - let msFromNow = (start + delta) - now; + let msFromNow = start + delta - now; if (msFromNow < 0) { msFromNow = 0; } @@ -784,7 +848,6 @@ console.log('message', this.get('sent_at'), 'expires at', expiresAt); } }, - }); Whisper.MessageCollection = Backbone.Collection.extend({ @@ -804,19 +867,29 @@ } }, destroyAll() { - return Promise.all(this.models.map(m => new Promise((resolve, reject) => { - m.destroy().then(resolve).fail(reject); - }))); + return Promise.all( + this.models.map( + m => + new Promise((resolve, reject) => { + m + .destroy() + .then(resolve) + .fail(reject); + }) + ) + ); }, fetchSentAt(timestamp) { - return new Promise((resolve => this.fetch({ - index: { - // 'receipt' index on sent_at - name: 'receipt', - only: timestamp, - }, - }).always(resolve))); + return new Promise(resolve => + this.fetch({ + index: { + // 'receipt' index on sent_at + name: 'receipt', + only: timestamp, + }, + }).always(resolve) + ); }, getLoadedUnreadCount() { @@ -841,7 +914,7 @@ if (unreadCount > 0) { startingLoadedUnread = this.getLoadedUnreadCount(); } - return new Promise((resolve) => { + return new Promise(resolve => { let upper; if (this.length === 0) { // fetch the most recent messages first @@ -893,4 +966,4 @@ }); }, }); -}()); +})(); diff --git a/js/modules/auto_orient_image.js b/js/modules/auto_orient_image.js index efe37da05..fbdabfd64 100644 --- a/js/modules/auto_orient_image.js +++ b/js/modules/auto_orient_image.js @@ -20,21 +20,25 @@ exports.autoOrientImage = (fileOrBlobOrURL, options = {}) => { ); return new Promise((resolve, reject) => { - loadImage(fileOrBlobOrURL, (canvasOrError) => { - if (canvasOrError.type === 'error') { - const error = new Error('autoOrientImage: Failed to process image'); - error.cause = canvasOrError; - reject(error); - return; - } + loadImage( + fileOrBlobOrURL, + canvasOrError => { + if (canvasOrError.type === 'error') { + const error = new Error('autoOrientImage: Failed to process image'); + error.cause = canvasOrError; + reject(error); + return; + } - const canvas = canvasOrError; - const dataURL = canvas.toDataURL( - optionsWithDefaults.type, - optionsWithDefaults.quality - ); + const canvas = canvasOrError; + const dataURL = canvas.toDataURL( + optionsWithDefaults.type, + optionsWithDefaults.quality + ); - resolve(dataURL); - }, optionsWithDefaults); + resolve(dataURL); + }, + optionsWithDefaults + ); }); }; diff --git a/js/modules/backup.js b/js/modules/backup.js index ead47b44a..523b63ff1 100644 --- a/js/modules/backup.js +++ b/js/modules/backup.js @@ -23,12 +23,7 @@ const electronRemote = require('electron').remote; const Attachment = require('./types/attachment'); const crypto = require('./crypto'); - -const { - dialog, - BrowserWindow, -} = electronRemote; - +const { dialog, BrowserWindow } = electronRemote; module.exports = { getDirectoryForExport, @@ -44,7 +39,6 @@ module.exports = { _getConversationLoggingName, }; - function stringify(object) { // eslint-disable-next-line no-restricted-syntax for (const key in object) { @@ -69,10 +63,12 @@ function unstringify(object) { // eslint-disable-next-line no-restricted-syntax for (const key in object) { const val = object[key]; - if (val && - val.type === 'ArrayBuffer' && - val.encoding === 'base64' && - typeof val.data === 'string') { + if ( + val && + val.type === 'ArrayBuffer' && + val.encoding === 'base64' && + typeof val.data === 'string' + ) { object[key] = dcodeIO.ByteBuffer.wrap(val.data, 'base64').toArrayBuffer(); } else if (val instanceof Object) { object[key] = unstringify(object[key]); @@ -86,19 +82,22 @@ function createOutputStream(writer) { return { write(string) { // eslint-disable-next-line more/no-then - wait = wait.then(() => new Promise((resolve) => { - if (writer.write(string)) { - resolve(); - return; - } + wait = wait.then( + () => + new Promise(resolve => { + if (writer.write(string)) { + resolve(); + return; + } - // If write() returns true, we don't need to wait for the drain event - // https://nodejs.org/dist/latest-v7.x/docs/api/stream.html#stream_class_stream_writable - writer.once('drain', resolve); + // If write() returns true, we don't need to wait for the drain event + // https://nodejs.org/dist/latest-v7.x/docs/api/stream.html#stream_class_stream_writable + writer.once('drain', resolve); - // We don't register for the 'error' event here, only in close(). Otherwise, - // we'll get "Possible EventEmitter memory leak detected" warnings. - })); + // We don't register for the 'error' event here, only in close(). Otherwise, + // we'll get "Possible EventEmitter memory leak detected" warnings. + }) + ); return wait; }, async close() { @@ -141,7 +140,7 @@ function exportContactsAndGroups(db, fileWriter) { stream.write('{'); - _.each(storeNames, (storeName) => { + _.each(storeNames, storeName => { // Both the readwrite permission and the multi-store transaction are required to // keep this function working. They serve to serialize all of these transactions, // one per store to be exported. @@ -167,7 +166,7 @@ function exportContactsAndGroups(db, fileWriter) { reject ); }; - request.onsuccess = async (event) => { + request.onsuccess = async event => { if (count === 0) { console.log('cursor opened'); stream.write(`"${storeName}": [`); @@ -180,10 +179,7 @@ function exportContactsAndGroups(db, fileWriter) { } // Preventing base64'd images from reaching the disk, making db.json too big - const item = _.omit( - cursor.value, - ['avatar', 'profileAvatar'] - ); + const item = _.omit(cursor.value, ['avatar', 'profileAvatar']); const jsonString = JSON.stringify(stringify(item)); stream.write(jsonString); @@ -235,10 +231,7 @@ function importFromJsonString(db, jsonString, targetPath, options) { groupLookup: {}, }); - const { - conversationLookup, - groupLookup, - } = options; + const { conversationLookup, groupLookup } = options; const result = { fullImport: true, }; @@ -269,7 +262,7 @@ function importFromJsonString(db, jsonString, targetPath, options) { console.log('Importing to these stores:', storeNames.join(', ')); let finished = false; - const finish = (via) => { + const finish = via => { console.log('non-messages import done via', via); if (finished) { resolve(result); @@ -287,7 +280,7 @@ function importFromJsonString(db, jsonString, targetPath, options) { }; transaction.oncomplete = finish.bind(null, 'transaction complete'); - _.each(storeNames, (storeName) => { + _.each(storeNames, storeName => { console.log('Importing items for store', storeName); if (!importObject[storeName].length) { @@ -316,14 +309,14 @@ function importFromJsonString(db, jsonString, targetPath, options) { } }; - _.each(importObject[storeName], (toAdd) => { + _.each(importObject[storeName], toAdd => { toAdd = unstringify(toAdd); const haveConversationAlready = - storeName === 'conversations' && - conversationLookup[getConversationKey(toAdd)]; + storeName === 'conversations' && + conversationLookup[getConversationKey(toAdd)]; const haveGroupAlready = - storeName === 'groups' && groupLookup[getGroupKey(toAdd)]; + storeName === 'groups' && groupLookup[getGroupKey(toAdd)]; if (haveConversationAlready || haveGroupAlready) { skipCount += 1; @@ -365,7 +358,7 @@ function createDirectory(parent, name) { return; } - fs.mkdir(targetDir, (error) => { + fs.mkdir(targetDir, error => { if (error) { reject(error); return; @@ -377,7 +370,7 @@ function createDirectory(parent, name) { } function createFileAndWriter(parent, name) { - return new Promise((resolve) => { + return new Promise(resolve => { const sanitized = _sanitizeFileName(name); const targetPath = path.join(parent, sanitized); const options = { @@ -430,7 +423,6 @@ function _trimFileName(filename) { return `${name.join('.').slice(0, 24)}.${extension}`; } - function _getExportAttachmentFileName(message, index, attachment) { if (attachment.fileName) { return _trimFileName(attachment.fileName); @@ -440,7 +432,9 @@ function _getExportAttachmentFileName(message, index, attachment) { if (attachment.contentType) { const components = attachment.contentType.split('/'); - name += `.${components.length > 1 ? components[1] : attachment.contentType}`; + name += `.${ + components.length > 1 ? components[1] : attachment.contentType + }`; } return name; @@ -477,14 +471,11 @@ async function readAttachment(dir, attachment, name, options) { } async function writeThumbnail(attachment, options) { - const { - dir, + const { dir, message, index, key, newKey } = options; + const filename = `${_getAnonymousAttachmentFileName( message, - index, - key, - newKey, - } = options; - const filename = `${_getAnonymousAttachmentFileName(message, index)}-thumbnail`; + index + )}-thumbnail`; const target = path.join(dir, filename); const { thumbnail } = attachment; @@ -504,26 +495,28 @@ async function writeThumbnails(rawQuotedAttachments, options) { const { name } = options; const { loadAttachmentData } = Signal.Migrations; - const promises = rawQuotedAttachments.map(async (attachment) => { + const promises = rawQuotedAttachments.map(async attachment => { if (!attachment || !attachment.thumbnail || !attachment.thumbnail.path) { return attachment; } - return Object.assign( - {}, - attachment, - { thumbnail: await loadAttachmentData(attachment.thumbnail) } - ); + return Object.assign({}, attachment, { + thumbnail: await loadAttachmentData(attachment.thumbnail), + }); }); const attachments = await Promise.all(promises); try { - await Promise.all(_.map( - attachments, - (attachment, index) => writeThumbnail(attachment, Object.assign({}, options, { - index, - })) - )); + await Promise.all( + _.map(attachments, (attachment, index) => + writeThumbnail( + attachment, + Object.assign({}, options, { + index, + }) + ) + ) + ); } catch (error) { console.log( 'writeThumbnails: error exporting conversation', @@ -536,13 +529,7 @@ async function writeThumbnails(rawQuotedAttachments, options) { } async function writeAttachment(attachment, options) { - const { - dir, - message, - index, - key, - newKey, - } = options; + const { dir, message, index, key, newKey } = options; const filename = _getAnonymousAttachmentFileName(message, index); const target = path.join(dir, filename); if (!Attachment.hasData(attachment)) { @@ -562,11 +549,13 @@ async function writeAttachments(rawAttachments, options) { const { loadAttachmentData } = Signal.Migrations; const attachments = await Promise.all(rawAttachments.map(loadAttachmentData)); - const promises = _.map( - attachments, - (attachment, index) => writeAttachment(attachment, Object.assign({}, options, { - index, - })) + const promises = _.map(attachments, (attachment, index) => + writeAttachment( + attachment, + Object.assign({}, options, { + index, + }) + ) ); try { await Promise.all(promises); @@ -582,12 +571,7 @@ async function writeAttachments(rawAttachments, options) { } async function writeEncryptedAttachment(target, data, options = {}) { - const { - key, - newKey, - filename, - dir, - } = options; + const { key, newKey, filename, dir } = options; if (fs.existsSync(target)) { if (newKey) { @@ -613,13 +597,7 @@ function _sanitizeFileName(filename) { async function exportConversation(db, conversation, options) { options = options || {}; - const { - name, - dir, - attachmentsDir, - key, - newKey, - } = options; + const { name, dir, attachmentsDir, key, newKey } = options; if (!name) { throw new Error('Need a name!'); } @@ -670,7 +648,7 @@ async function exportConversation(db, conversation, options) { reject ); }; - request.onsuccess = async (event) => { + request.onsuccess = async event => { const cursor = event.target.result; if (cursor) { const message = cursor.value; @@ -688,13 +666,12 @@ async function exportConversation(db, conversation, options) { // eliminate attachment data from the JSON, since it will go to disk // Note: this is for legacy messages only, which stored attachment data in the db - message.attachments = _.map( - attachments, - attachment => _.omit(attachment, ['data']) + message.attachments = _.map(attachments, attachment => + _.omit(attachment, ['data']) ); // completely drop any attachments in messages cached in error objects // TODO: move to lodash. Sadly, a number of the method signatures have changed! - message.errors = _.map(message.errors, (error) => { + message.errors = _.map(message.errors, error => { if (error && error.args) { error.args = []; } @@ -709,13 +686,14 @@ async function exportConversation(db, conversation, options) { console.log({ backupMessage: message }); if (attachments && attachments.length > 0) { - const exportAttachments = () => writeAttachments(attachments, { - dir: attachmentsDir, - name, - message, - key, - newKey, - }); + const exportAttachments = () => + writeAttachments(attachments, { + dir: attachmentsDir, + name, + message, + key, + newKey, + }); // eslint-disable-next-line more/no-then promiseChain = promiseChain.then(exportAttachments); @@ -723,13 +701,14 @@ async function exportConversation(db, conversation, options) { const quoteThumbnails = message.quote && message.quote.attachments; if (quoteThumbnails && quoteThumbnails.length > 0) { - const exportQuoteThumbnails = () => writeThumbnails(quoteThumbnails, { - dir: attachmentsDir, - name, - message, - key, - newKey, - }); + const exportQuoteThumbnails = () => + writeThumbnails(quoteThumbnails, { + dir: attachmentsDir, + name, + message, + key, + newKey, + }); // eslint-disable-next-line more/no-then promiseChain = promiseChain.then(exportQuoteThumbnails); @@ -739,11 +718,7 @@ async function exportConversation(db, conversation, options) { cursor.continue(); } else { try { - await Promise.all([ - stream.write(']}'), - promiseChain, - stream.close(), - ]); + await Promise.all([stream.write(']}'), promiseChain, stream.close()]); } catch (error) { console.log( 'exportConversation: error exporting conversation', @@ -791,12 +766,7 @@ function _getConversationLoggingName(conversation) { function exportConversations(db, options) { options = options || {}; - const { - messagesDir, - attachmentsDir, - key, - newKey, - } = options; + const { messagesDir, attachmentsDir, key, newKey } = options; if (!messagesDir) { return Promise.reject(new Error('Need a messages directory!')); @@ -828,7 +798,7 @@ function exportConversations(db, options) { reject ); }; - request.onsuccess = async (event) => { + request.onsuccess = async event => { const cursor = event.target.result; if (cursor && cursor.value) { const conversation = cursor.value; @@ -873,7 +843,7 @@ function getDirectory(options) { buttonLabel: options.buttonLabel, }; - dialog.showOpenDialog(browserWindow, dialogOptions, (directory) => { + dialog.showOpenDialog(browserWindow, dialogOptions, directory => { if (!directory || !directory[0]) { const error = new Error('Error choosing directory'); error.name = 'ChooseError'; @@ -940,7 +910,7 @@ async function saveAllMessages(db, rawMessages) { return new Promise((resolve, reject) => { let finished = false; - const finish = (via) => { + const finish = via => { console.log('messages done saving via', via); if (finished) { resolve(); @@ -962,7 +932,7 @@ async function saveAllMessages(db, rawMessages) { const { conversationId } = messages[0]; let count = 0; - _.forEach(messages, (message) => { + _.forEach(messages, message => { const request = store.put(message, message.id); request.onsuccess = () => { count += 1; @@ -997,11 +967,7 @@ async function importConversation(db, dir, options) { options = options || {}; _.defaults(options, { messageLookup: {} }); - const { - messageLookup, - attachmentsDir, - key, - } = options; + const { messageLookup, attachmentsDir, key } = options; let conversationId = 'unknown'; let total = 0; @@ -1018,11 +984,13 @@ async function importConversation(db, dir, options) { const json = JSON.parse(contents); if (json.messages && json.messages.length) { - conversationId = `[REDACTED]${(json.messages[0].conversationId || '').slice(-3)}`; + conversationId = `[REDACTED]${(json.messages[0].conversationId || '').slice( + -3 + )}`; } total = json.messages.length; - const messages = _.filter(json.messages, (message) => { + const messages = _.filter(json.messages, message => { message = unstringify(message); if (messageLookup[getMessageKey(message)]) { @@ -1031,7 +999,9 @@ async function importConversation(db, dir, options) { } const hasAttachments = message.attachments && message.attachments.length; - const hasQuotedAttachments = message.quote && message.quote.attachments && + const hasQuotedAttachments = + message.quote && + message.quote.attachments && message.quote.attachments.length > 0; if (hasAttachments || hasQuotedAttachments) { @@ -1039,8 +1009,8 @@ async function importConversation(db, dir, options) { const getName = attachmentsDir ? _getAnonymousAttachmentFileName : _getExportAttachmentFileName; - const parentDir = attachmentsDir || - path.join(dir, message.received_at.toString()); + const parentDir = + attachmentsDir || path.join(dir, message.received_at.toString()); await loadAttachments(parentDir, getName, { message, @@ -1075,12 +1045,13 @@ async function importConversations(db, dir, options) { const contents = await getDirContents(dir); let promiseChain = Promise.resolve(); - _.forEach(contents, (conversationDir) => { + _.forEach(contents, conversationDir => { if (!fs.statSync(conversationDir).isDirectory()) { return; } - const loadConversation = () => importConversation(db, conversationDir, options); + const loadConversation = () => + importConversation(db, conversationDir, options); // eslint-disable-next-line more/no-then promiseChain = promiseChain.then(loadConversation); @@ -1142,7 +1113,7 @@ function assembleLookup(db, storeName, keyFunction) { reject ); }; - request.onsuccess = (event) => { + request.onsuccess = event => { const cursor = event.target.result; if (cursor && cursor.value) { lookup[keyFunction(cursor.value)] = true; @@ -1175,7 +1146,7 @@ function createZip(zipDir, targetDir) { resolve(target); }); - archive.on('warning', (error) => { + archive.on('warning', error => { console.log(`Archive generation warning: ${error.stack}`); }); archive.on('error', reject); @@ -1247,10 +1218,13 @@ async function exportToDirectory(directory, options) { const attachmentsDir = await createDirectory(directory, 'attachments'); await exportContactAndGroupsToFile(db, stagingDir); - await exportConversations(db, Object.assign({}, options, { - messagesDir: stagingDir, - attachmentsDir, - })); + await exportConversations( + db, + Object.assign({}, options, { + messagesDir: stagingDir, + attachmentsDir, + }) + ); const zip = await createZip(encryptionDir, stagingDir); await encryptFile(zip, path.join(directory, 'messages.zip'), options); @@ -1302,7 +1276,9 @@ async function importFromDirectory(directory, options) { if (fs.existsSync(zipPath)) { // we're in the world of an encrypted, zipped backup if (!options.key) { - throw new Error('Importing an encrypted backup; decryption key is required!'); + throw new Error( + 'Importing an encrypted backup; decryption key is required!' + ); } let stagingDir; diff --git a/js/modules/crypto.js b/js/modules/crypto.js index b4605ebcd..f289c01e2 100644 --- a/js/modules/crypto.js +++ b/js/modules/crypto.js @@ -19,8 +19,15 @@ async function encryptSymmetric(key, plaintext) { const cipherKey = await _hmac_SHA256(key, nonce); const macKey = await _hmac_SHA256(key, cipherKey); - const cipherText = await _encrypt_aes256_CBC_PKCSPadding(cipherKey, iv, plaintext); - const mac = _getFirstBytes(await _hmac_SHA256(macKey, cipherText), MAC_LENGTH); + const cipherText = await _encrypt_aes256_CBC_PKCSPadding( + cipherKey, + iv, + plaintext + ); + const mac = _getFirstBytes( + await _hmac_SHA256(macKey, cipherText), + MAC_LENGTH + ); return _concatData([nonce, cipherText, mac]); } @@ -39,9 +46,14 @@ async function decryptSymmetric(key, data) { const cipherKey = await _hmac_SHA256(key, nonce); const macKey = await _hmac_SHA256(key, cipherKey); - const ourMac = _getFirstBytes(await _hmac_SHA256(macKey, cipherText), MAC_LENGTH); + const ourMac = _getFirstBytes( + await _hmac_SHA256(macKey, cipherText), + MAC_LENGTH + ); if (!constantTimeEqual(theirMac, ourMac)) { - throw new Error('decryptSymmetric: Failed to decrypt; MAC verification failed'); + throw new Error( + 'decryptSymmetric: Failed to decrypt; MAC verification failed' + ); } return _decrypt_aes256_CBC_PKCSPadding(cipherKey, iv, cipherText); @@ -61,7 +73,6 @@ function constantTimeEqual(left, right) { return result === 0; } - async function _hmac_SHA256(key, data) { const extractable = false; const cryptoKey = await window.crypto.subtle.importKey( @@ -72,7 +83,11 @@ async function _hmac_SHA256(key, data) { ['sign'] ); - return window.crypto.subtle.sign({ name: 'HMAC', hash: 'SHA-256' }, cryptoKey, data); + return window.crypto.subtle.sign( + { name: 'HMAC', hash: 'SHA-256' }, + cryptoKey, + data + ); } async function _encrypt_aes256_CBC_PKCSPadding(key, iv, data) { @@ -101,7 +116,6 @@ async function _decrypt_aes256_CBC_PKCSPadding(key, iv, data) { return window.crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, data); } - function _getRandomBytes(n) { const bytes = new Uint8Array(n); window.crypto.getRandomValues(bytes); diff --git a/js/modules/database.js b/js/modules/database.js index e8527d254..18c3845f7 100644 --- a/js/modules/database.js +++ b/js/modules/database.js @@ -6,14 +6,12 @@ const { isObject, isNumber } = require('lodash'); - exports.open = (name, version, { onUpgradeNeeded } = {}) => { const request = indexedDB.open(name, version); return new Promise((resolve, reject) => { - request.onblocked = () => - reject(new Error('Database blocked')); + request.onblocked = () => reject(new Error('Database blocked')); - request.onupgradeneeded = (event) => { + request.onupgradeneeded = event => { const hasRequestedSpecificVersion = isNumber(version); if (!hasRequestedSpecificVersion) { return; @@ -26,14 +24,17 @@ exports.open = (name, version, { onUpgradeNeeded } = {}) => { return; } - reject(new Error('Database upgrade required:' + - ` oldVersion: ${oldVersion}, newVersion: ${newVersion}`)); + reject( + new Error( + 'Database upgrade required:' + + ` oldVersion: ${oldVersion}, newVersion: ${newVersion}` + ) + ); }; - request.onerror = event => - reject(event.target.error); + request.onerror = event => reject(event.target.error); - request.onsuccess = (event) => { + request.onsuccess = event => { const connection = event.target.result; resolve(connection); }; @@ -47,7 +48,7 @@ exports.completeTransaction = transaction => transaction.addEventListener('complete', () => resolve()); }); -exports.getVersion = async (name) => { +exports.getVersion = async name => { const connection = await exports.open(name); const { version } = connection; connection.close(); @@ -61,9 +62,7 @@ exports.getCount = async ({ store } = {}) => { const request = store.count(); return new Promise((resolve, reject) => { - request.onerror = event => - reject(event.target.error); - request.onsuccess = event => - resolve(event.target.result); + request.onerror = event => reject(event.target.error); + request.onsuccess = event => resolve(event.target.result); }); }; diff --git a/js/modules/debug.js b/js/modules/debug.js index 5de466bca..4a1f7035c 100644 --- a/js/modules/debug.js +++ b/js/modules/debug.js @@ -18,7 +18,6 @@ const Message = require('./types/message'); const { deferredToPromise } = require('./deferred_to_promise'); const { sleep } = require('./sleep'); - // See: https://en.wikipedia.org/wiki/Fictitious_telephone_number#North_American_Numbering_Plan const SENDER_ID = '+12126647665'; @@ -27,8 +26,10 @@ exports.createConversation = async ({ numMessages, WhisperMessage, } = {}) => { - if (!isObject(ConversationController) || - !isFunction(ConversationController.getOrCreateAndWait)) { + if ( + !isObject(ConversationController) || + !isFunction(ConversationController.getOrCreateAndWait) + ) { throw new TypeError("'ConversationController' is required"); } @@ -40,8 +41,10 @@ exports.createConversation = async ({ throw new TypeError("'WhisperMessage' is required"); } - const conversation = - await ConversationController.getOrCreateAndWait(SENDER_ID, 'private'); + const conversation = await ConversationController.getOrCreateAndWait( + SENDER_ID, + 'private' + ); conversation.set({ active_at: Date.now(), unread: numMessages, @@ -50,13 +53,15 @@ exports.createConversation = async ({ const conversationId = conversation.get('id'); - await Promise.all(range(0, numMessages).map(async (index) => { - await sleep(index * 100); - console.log(`Create message ${index + 1}`); - const messageAttributes = await createRandomMessage({ conversationId }); - const message = new WhisperMessage(messageAttributes); - return deferredToPromise(message.save()); - })); + await Promise.all( + range(0, numMessages).map(async index => { + await sleep(index * 100); + console.log(`Create message ${index + 1}`); + const messageAttributes = await createRandomMessage({ conversationId }); + const message = new WhisperMessage(messageAttributes); + return deferredToPromise(message.save()); + }) + ); }; const SAMPLE_MESSAGES = [ @@ -88,7 +93,8 @@ const createRandomMessage = async ({ conversationId } = {}) => { const hasAttachment = Math.random() <= ATTACHMENT_SAMPLE_RATE; const attachments = hasAttachment - ? [await createRandomInMemoryAttachment()] : []; + ? [await createRandomInMemoryAttachment()] + : []; const type = sample(['incoming', 'outgoing']); const commonProperties = { attachments, @@ -145,7 +151,7 @@ const createFileEntry = fileName => ({ fileName, contentType: fileNameToContentType(fileName), }); -const fileNameToContentType = (fileName) => { +const fileNameToContentType = fileName => { const fileExtension = path.extname(fileName).toLowerCase(); switch (fileExtension) { case '.gif': diff --git a/js/modules/debuglogs.js b/js/modules/debuglogs.js index 44bb443f9..290cb4ac9 100644 --- a/js/modules/debuglogs.js +++ b/js/modules/debuglogs.js @@ -3,7 +3,6 @@ const FormData = require('form-data'); const got = require('got'); - const BASE_URL = 'https://debuglogs.org'; // Workaround: Submitting `FormData` using native `FormData::submit` procedure @@ -12,7 +11,7 @@ const BASE_URL = 'https://debuglogs.org'; // https://github.com/sindresorhus/got/pull/466 const submitFormData = (form, url) => new Promise((resolve, reject) => { - form.submit(url, (error) => { + form.submit(url, error => { if (error) { return reject(error); } @@ -22,7 +21,7 @@ const submitFormData = (form, url) => }); // upload :: String -> Promise URL -exports.upload = async (content) => { +exports.upload = async content => { const signedForm = await got.get(BASE_URL, { json: true }); const { fields, url } = signedForm.body; diff --git a/js/modules/global_errors.js b/js/modules/global_errors.js index b868bd0a5..d7b2279b7 100644 --- a/js/modules/global_errors.js +++ b/js/modules/global_errors.js @@ -2,11 +2,10 @@ const addUnhandledErrorHandler = require('electron-unhandled'); const Errors = require('./types/errors'); - // addHandler :: Unit -> Unit exports.addHandler = () => { addUnhandledErrorHandler({ - logger: (error) => { + logger: error => { console.error( 'Uncaught error or unhandled promise rejection:', Errors.toLogFormat(error) diff --git a/js/modules/i18n.js b/js/modules/i18n.js index 09a5eb96c..f10ecda05 100644 --- a/js/modules/i18n.js +++ b/js/modules/i18n.js @@ -11,7 +11,9 @@ exports.setup = (locale, messages) => { function getMessage(key, substitutions) { const entry = messages[key]; if (!entry) { - console.error(`i18n: Attempted to get translation for nonexistent key '${key}'`); + console.error( + `i18n: Attempted to get translation for nonexistent key '${key}'` + ); return ''; } diff --git a/js/modules/idle_detector.js b/js/modules/idle_detector.js index 9ccd3ce6b..be06903c6 100644 --- a/js/modules/idle_detector.js +++ b/js/modules/idle_detector.js @@ -2,7 +2,6 @@ const EventEmitter = require('events'); - const POLL_INTERVAL_MS = 5 * 1000; const IDLE_THRESHOLD_MS = 20; @@ -35,14 +34,17 @@ class IdleDetector extends EventEmitter { _scheduleNextCallback() { this._clearScheduledCallbacks(); - this.handle = window.requestIdleCallback((deadline) => { + this.handle = window.requestIdleCallback(deadline => { const { didTimeout } = deadline; const timeRemaining = deadline.timeRemaining(); const isIdle = timeRemaining >= IDLE_THRESHOLD_MS; if (isIdle || didTimeout) { this.emit('idle', { timestamp: Date.now(), didTimeout, timeRemaining }); } - this.timeoutId = setTimeout(() => this._scheduleNextCallback(), POLL_INTERVAL_MS); + this.timeoutId = setTimeout( + () => this._scheduleNextCallback(), + POLL_INTERVAL_MS + ); }); } } diff --git a/js/modules/link_text.js b/js/modules/link_text.js index 5354f3da8..9114c237d 100644 --- a/js/modules/link_text.js +++ b/js/modules/link_text.js @@ -7,7 +7,7 @@ function createLink(url, text, attrs = {}) { const html = []; html.push(' { + Object.keys(attrs).forEach(key => { html.push(` ${key}="${attrs[key]}"`); }); html.push('>'); @@ -23,7 +23,7 @@ module.exports = (text, attrs = {}) => { const result = []; let last = 0; - matchData.forEach((match) => { + matchData.forEach(match => { if (last < match.index) { result.push(text.slice(last, match.index)); } diff --git a/js/modules/messages_data_migrator.js b/js/modules/messages_data_migrator.js index 3f70f0f87..849227f9d 100644 --- a/js/modules/messages_data_migrator.js +++ b/js/modules/messages_data_migrator.js @@ -6,20 +6,13 @@ /* global IDBKeyRange */ -const { - isFunction, - isNumber, - isObject, - isString, - last, -} = require('lodash'); +const { isFunction, isNumber, isObject, isString, last } = require('lodash'); const database = require('./database'); const Message = require('./types/message'); const settings = require('./settings'); const { deferredToPromise } = require('./deferred_to_promise'); - const MESSAGES_STORE_NAME = 'messages'; exports.processNext = async ({ @@ -29,12 +22,16 @@ exports.processNext = async ({ upgradeMessageSchema, } = {}) => { if (!isFunction(BackboneMessage)) { - throw new TypeError("'BackboneMessage' (Whisper.Message) constructor is required"); + throw new TypeError( + "'BackboneMessage' (Whisper.Message) constructor is required" + ); } if (!isFunction(BackboneMessageCollection)) { - throw new TypeError("'BackboneMessageCollection' (Whisper.MessageCollection)" + - ' constructor is required'); + throw new TypeError( + "'BackboneMessageCollection' (Whisper.MessageCollection)" + + ' constructor is required' + ); } if (!isNumber(numMessagesPerBatch)) { @@ -48,16 +45,18 @@ exports.processNext = async ({ const startTime = Date.now(); const fetchStartTime = Date.now(); - const messagesRequiringSchemaUpgrade = - await _fetchMessagesRequiringSchemaUpgrade({ + const messagesRequiringSchemaUpgrade = await _fetchMessagesRequiringSchemaUpgrade( + { BackboneMessageCollection, count: numMessagesPerBatch, - }); + } + ); const fetchDuration = Date.now() - fetchStartTime; const upgradeStartTime = Date.now(); - const upgradedMessages = - await Promise.all(messagesRequiringSchemaUpgrade.map(upgradeMessageSchema)); + const upgradedMessages = await Promise.all( + messagesRequiringSchemaUpgrade.map(upgradeMessageSchema) + ); const upgradeDuration = Date.now() - upgradeStartTime; const saveStartTime = Date.now(); @@ -109,8 +108,10 @@ exports.dangerouslyProcessAllWithoutIndex = async ({ minDatabaseVersion, }); if (!isValidDatabaseVersion) { - throw new Error(`Expected database version (${databaseVersion})` + - ` to be at least ${minDatabaseVersion}`); + throw new Error( + `Expected database version (${databaseVersion})` + + ` to be at least ${minDatabaseVersion}` + ); } // NOTE: Even if we make this async using `then`, requesting `count` on an @@ -132,10 +133,13 @@ exports.dangerouslyProcessAllWithoutIndex = async ({ break; } numCumulativeMessagesProcessed += status.numMessagesProcessed; - console.log('Upgrade message schema:', Object.assign({}, status, { - numTotalMessages, - numCumulativeMessagesProcessed, - })); + console.log( + 'Upgrade message schema:', + Object.assign({}, status, { + numTotalMessages, + numCumulativeMessagesProcessed, + }) + ); } console.log('Close database connection'); @@ -181,8 +185,10 @@ const _getConnection = async ({ databaseName, minDatabaseVersion }) => { const databaseVersion = connection.version; const isValidDatabaseVersion = databaseVersion >= minDatabaseVersion; if (!isValidDatabaseVersion) { - throw new Error(`Expected database version (${databaseVersion})` + - ` to be at least ${minDatabaseVersion}`); + throw new Error( + `Expected database version (${databaseVersion})` + + ` to be at least ${minDatabaseVersion}` + ); } return connection; @@ -205,29 +211,33 @@ const _processBatch = async ({ throw new TypeError("'numMessagesPerBatch' is required"); } - const isAttachmentMigrationComplete = - await settings.isAttachmentMigrationComplete(connection); + const isAttachmentMigrationComplete = await settings.isAttachmentMigrationComplete( + connection + ); if (isAttachmentMigrationComplete) { return { done: true, }; } - const lastProcessedIndex = - await settings.getAttachmentMigrationLastProcessedIndex(connection); + const lastProcessedIndex = await settings.getAttachmentMigrationLastProcessedIndex( + connection + ); const fetchUnprocessedMessagesStartTime = Date.now(); - const unprocessedMessages = - await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex({ + const unprocessedMessages = await _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex( + { connection, count: numMessagesPerBatch, lastIndex: lastProcessedIndex, - }); + } + ); const fetchDuration = Date.now() - fetchUnprocessedMessagesStartTime; const upgradeStartTime = Date.now(); - const upgradedMessages = - await Promise.all(unprocessedMessages.map(upgradeMessageSchema)); + const upgradedMessages = await Promise.all( + unprocessedMessages.map(upgradeMessageSchema) + ); const upgradeDuration = Date.now() - upgradeStartTime; const saveMessagesStartTime = Date.now(); @@ -266,12 +276,12 @@ const _processBatch = async ({ }; }; -const _saveMessageBackbone = ({ BackboneMessage } = {}) => (message) => { +const _saveMessageBackbone = ({ BackboneMessage } = {}) => message => { const backboneMessage = new BackboneMessage(message); return deferredToPromise(backboneMessage.save()); }; -const _saveMessage = ({ transaction } = {}) => (message) => { +const _saveMessage = ({ transaction } = {}) => message => { if (!isObject(transaction)) { throw new TypeError("'transaction' is required"); } @@ -279,83 +289,91 @@ const _saveMessage = ({ transaction } = {}) => (message) => { const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME); const request = messagesStore.put(message, message.id); return new Promise((resolve, reject) => { - request.onsuccess = () => - resolve(); - request.onerror = event => - reject(event.target.error); + request.onsuccess = () => resolve(); + request.onerror = event => reject(event.target.error); }); }; -const _fetchMessagesRequiringSchemaUpgrade = - async ({ BackboneMessageCollection, count } = {}) => { - if (!isFunction(BackboneMessageCollection)) { - throw new TypeError("'BackboneMessageCollection' (Whisper.MessageCollection)" + - ' constructor is required'); - } +const _fetchMessagesRequiringSchemaUpgrade = async ({ + BackboneMessageCollection, + count, +} = {}) => { + if (!isFunction(BackboneMessageCollection)) { + throw new TypeError( + "'BackboneMessageCollection' (Whisper.MessageCollection)" + + ' constructor is required' + ); + } - if (!isNumber(count)) { - throw new TypeError("'count' is required"); - } + if (!isNumber(count)) { + throw new TypeError("'count' is required"); + } - const collection = new BackboneMessageCollection(); - return new Promise(resolve => collection.fetch({ - limit: count, - index: { - name: 'schemaVersion', - upper: Message.CURRENT_SCHEMA_VERSION, - excludeUpper: true, - order: 'desc', - }, - }).always(() => { - const models = collection.models || []; - const messages = models.map(model => model.toJSON()); - resolve(messages); - })); - }; + const collection = new BackboneMessageCollection(); + return new Promise(resolve => + collection + .fetch({ + limit: count, + index: { + name: 'schemaVersion', + upper: Message.CURRENT_SCHEMA_VERSION, + excludeUpper: true, + order: 'desc', + }, + }) + .always(() => { + const models = collection.models || []; + const messages = models.map(model => model.toJSON()); + resolve(messages); + }) + ); +}; // NOTE: Named ‘dangerous’ because it is not as efficient as using our // `messages` `schemaVersion` index: -const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = - ({ connection, count, lastIndex } = {}) => { - if (!isObject(connection)) { - throw new TypeError("'connection' is required"); - } +const _dangerouslyFetchMessagesRequiringSchemaUpgradeWithoutIndex = ({ + connection, + count, + lastIndex, +} = {}) => { + if (!isObject(connection)) { + throw new TypeError("'connection' is required"); + } - if (!isNumber(count)) { - throw new TypeError("'count' is required"); - } + if (!isNumber(count)) { + throw new TypeError("'count' is required"); + } - if (lastIndex && !isString(lastIndex)) { - throw new TypeError("'lastIndex' must be a string"); - } + if (lastIndex && !isString(lastIndex)) { + throw new TypeError("'lastIndex' must be a string"); + } - const hasLastIndex = Boolean(lastIndex); + const hasLastIndex = Boolean(lastIndex); - const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readonly'); - const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME); + const transaction = connection.transaction(MESSAGES_STORE_NAME, 'readonly'); + const messagesStore = transaction.objectStore(MESSAGES_STORE_NAME); - const excludeLowerBound = true; - const range = hasLastIndex - ? IDBKeyRange.lowerBound(lastIndex, excludeLowerBound) - : undefined; - return new Promise((resolve, reject) => { - const items = []; - const request = messagesStore.openCursor(range); - request.onsuccess = (event) => { - const cursor = event.target.result; - const hasMoreData = Boolean(cursor); - if (!hasMoreData || items.length === count) { - resolve(items); - return; - } - const item = cursor.value; - items.push(item); - cursor.continue(); - }; - request.onerror = event => - reject(event.target.error); - }); - }; + const excludeLowerBound = true; + const range = hasLastIndex + ? IDBKeyRange.lowerBound(lastIndex, excludeLowerBound) + : undefined; + return new Promise((resolve, reject) => { + const items = []; + const request = messagesStore.openCursor(range); + request.onsuccess = event => { + const cursor = event.target.result; + const hasMoreData = Boolean(cursor); + if (!hasMoreData || items.length === count) { + resolve(items); + return; + } + const item = cursor.value; + items.push(item); + cursor.continue(); + }; + request.onerror = event => reject(event.target.error); + }); +}; const _getNumMessages = async ({ connection } = {}) => { if (!isObject(connection)) { diff --git a/js/modules/migrations/18/index.js b/js/modules/migrations/18/index.js index 9ffeffb21..4db6633d3 100644 --- a/js/modules/migrations/18/index.js +++ b/js/modules/migrations/18/index.js @@ -1,4 +1,4 @@ -exports.run = (transaction) => { +exports.run = transaction => { const messagesStore = transaction.objectStore('messages'); console.log("Create message attachment metadata index: 'hasAttachments'"); @@ -8,12 +8,10 @@ exports.run = (transaction) => { { unique: false } ); - ['hasVisualMediaAttachments', 'hasFileAttachments'].forEach((name) => { + ['hasVisualMediaAttachments', 'hasFileAttachments'].forEach(name => { console.log(`Create message attachment metadata index: '${name}'`); - messagesStore.createIndex( - name, - ['conversationId', 'received_at', name], - { unique: false } - ); + messagesStore.createIndex(name, ['conversationId', 'received_at', name], { + unique: false, + }); }); }; diff --git a/js/modules/migrations/get_placeholder_migrations.js b/js/modules/migrations/get_placeholder_migrations.js index 2cd563e4b..5e41ce914 100644 --- a/js/modules/migrations/get_placeholder_migrations.js +++ b/js/modules/migrations/get_placeholder_migrations.js @@ -1,23 +1,22 @@ -const Migrations0DatabaseWithAttachmentData = - require('./migrations_0_database_with_attachment_data'); -const Migrations1DatabaseWithoutAttachmentData = - require('./migrations_1_database_without_attachment_data'); - +const Migrations0DatabaseWithAttachmentData = require('./migrations_0_database_with_attachment_data'); +const Migrations1DatabaseWithoutAttachmentData = require('./migrations_1_database_without_attachment_data'); exports.getPlaceholderMigrations = () => { - const last0MigrationVersion = - Migrations0DatabaseWithAttachmentData.getLatestVersion(); - const last1MigrationVersion = - Migrations1DatabaseWithoutAttachmentData.getLatestVersion(); + const last0MigrationVersion = Migrations0DatabaseWithAttachmentData.getLatestVersion(); + const last1MigrationVersion = Migrations1DatabaseWithoutAttachmentData.getLatestVersion(); const lastMigrationVersion = last1MigrationVersion || last0MigrationVersion; - return [{ - version: lastMigrationVersion, - migrate() { - throw new Error('Unexpected invocation of placeholder migration!' + - '\n\nMigrations must explicitly be run upon application startup instead' + - ' of implicitly via Backbone IndexedDB adapter at any time.'); + return [ + { + version: lastMigrationVersion, + migrate() { + throw new Error( + 'Unexpected invocation of placeholder migration!' + + '\n\nMigrations must explicitly be run upon application startup instead' + + ' of implicitly via Backbone IndexedDB adapter at any time.' + ); + }, }, - }]; + ]; }; diff --git a/js/modules/migrations/migrations_0_database_with_attachment_data.js b/js/modules/migrations/migrations_0_database_with_attachment_data.js index 953d8406e..71b935fef 100644 --- a/js/modules/migrations/migrations_0_database_with_attachment_data.js +++ b/js/modules/migrations/migrations_0_database_with_attachment_data.js @@ -3,7 +3,6 @@ const { isString, last } = require('lodash'); const { runMigrations } = require('./run_migrations'); const Migration18 = require('./18'); - // IMPORTANT: The migrations below are run on a database that may be very large // due to attachments being directly stored inside the database. Please avoid // any expensive operations, e.g. modifying all messages / attachments, etc., as @@ -20,7 +19,9 @@ const migrations = [ unique: false, }); messages.createIndex('receipt', 'sent_at', { unique: false }); - messages.createIndex('unread', ['conversationId', 'unread'], { unique: false }); + messages.createIndex('unread', ['conversationId', 'unread'], { + unique: false, + }); messages.createIndex('expires_at', 'expires_at', { unique: false }); const conversations = transaction.db.createObjectStore('conversations'); @@ -59,7 +60,7 @@ const migrations = [ const identityKeys = transaction.objectStore('identityKeys'); const request = identityKeys.openCursor(); const promises = []; - request.onsuccess = (event) => { + request.onsuccess = event => { const cursor = event.target.result; if (cursor) { const attributes = cursor.value; @@ -67,14 +68,16 @@ const migrations = [ attributes.firstUse = false; attributes.nonblockingApproval = false; attributes.verified = 0; - promises.push(new Promise(((resolve, reject) => { - const putRequest = identityKeys.put(attributes, attributes.id); - putRequest.onsuccess = resolve; - putRequest.onerror = (e) => { - console.log(e); - reject(e); - }; - }))); + promises.push( + new Promise((resolve, reject) => { + const putRequest = identityKeys.put(attributes, attributes.id); + putRequest.onsuccess = resolve; + putRequest.onerror = e => { + console.log(e); + reject(e); + }; + }) + ); cursor.continue(); } else { // no more results @@ -84,7 +87,7 @@ const migrations = [ }); } }; - request.onerror = (event) => { + request.onerror = event => { console.log(event); }; }, @@ -129,7 +132,9 @@ const migrations = [ const messagesStore = transaction.objectStore('messages'); console.log('Create index from attachment schema version to attachment'); - messagesStore.createIndex('schemaVersion', 'schemaVersion', { unique: false }); + messagesStore.createIndex('schemaVersion', 'schemaVersion', { + unique: false, + }); const duration = Date.now() - start; diff --git a/js/modules/migrations/migrations_1_database_without_attachment_data.js b/js/modules/migrations/migrations_1_database_without_attachment_data.js index fd1feeb17..df746f7c8 100644 --- a/js/modules/migrations/migrations_1_database_without_attachment_data.js +++ b/js/modules/migrations/migrations_1_database_without_attachment_data.js @@ -4,7 +4,6 @@ const db = require('../database'); const settings = require('../settings'); const { runMigrations } = require('./run_migrations'); - // IMPORTANT: Add new migrations that need to traverse entire database, e.g. // messages store, below. Whenever we need this, we need to force attachment // migration on startup: @@ -20,7 +19,9 @@ const migrations = [ exports.run = async ({ Backbone, database } = {}) => { const { canRun } = await exports.getStatus({ database }); if (!canRun) { - throw new Error('Cannot run migrations on database without attachment data'); + throw new Error( + 'Cannot run migrations on database without attachment data' + ); } await runMigrations({ Backbone, database }); @@ -28,8 +29,9 @@ exports.run = async ({ Backbone, database } = {}) => { exports.getStatus = async ({ database } = {}) => { const connection = await db.open(database.id, database.version); - const isAttachmentMigrationComplete = - await settings.isAttachmentMigrationComplete(connection); + const isAttachmentMigrationComplete = await settings.isAttachmentMigrationComplete( + connection + ); const hasMigrations = migrations.length > 0; const canRun = isAttachmentMigrationComplete && hasMigrations; diff --git a/js/modules/migrations/run_migrations.js b/js/modules/migrations/run_migrations.js index 9db4d5020..8e34f0001 100644 --- a/js/modules/migrations/run_migrations.js +++ b/js/modules/migrations/run_migrations.js @@ -1,29 +1,27 @@ /* eslint-env browser */ -const { - head, - isFunction, - isObject, - isString, - last, -} = require('lodash'); - +const { head, isFunction, isObject, isString, last } = require('lodash'); const db = require('../database'); const { deferredToPromise } = require('../deferred_to_promise'); - const closeDatabaseConnection = ({ Backbone } = {}) => deferredToPromise(Backbone.sync('closeall')); exports.runMigrations = async ({ Backbone, database } = {}) => { - if (!isObject(Backbone) || !isObject(Backbone.Collection) || - !isFunction(Backbone.Collection.extend)) { + if ( + !isObject(Backbone) || + !isObject(Backbone.Collection) || + !isFunction(Backbone.Collection.extend) + ) { throw new TypeError("'Backbone' is required"); } - if (!isObject(database) || !isString(database.id) || - !Array.isArray(database.migrations)) { + if ( + !isObject(database) || + !isString(database.id) || + !Array.isArray(database.migrations) + ) { throw new TypeError("'database' is required"); } @@ -56,7 +54,7 @@ exports.runMigrations = async ({ Backbone, database } = {}) => { await closeDatabaseConnection({ Backbone }); }; -const getMigrationVersions = (database) => { +const getMigrationVersions = database => { if (!isObject(database) || !Array.isArray(database.migrations)) { throw new TypeError("'database' is required"); } @@ -64,8 +62,12 @@ const getMigrationVersions = (database) => { const firstMigration = head(database.migrations); const lastMigration = last(database.migrations); - const firstVersion = firstMigration ? parseInt(firstMigration.version, 10) : null; - const lastVersion = lastMigration ? parseInt(lastMigration.version, 10) : null; + const firstVersion = firstMigration + ? parseInt(firstMigration.version, 10) + : null; + const lastVersion = lastMigration + ? parseInt(lastMigration.version, 10) + : null; return { firstVersion, lastVersion }; }; diff --git a/js/modules/os.js b/js/modules/os.js index d0266bc96..245172f68 100644 --- a/js/modules/os.js +++ b/js/modules/os.js @@ -1,10 +1,7 @@ /* eslint-env node */ -exports.isMacOS = () => - process.platform === 'darwin'; +exports.isMacOS = () => process.platform === 'darwin'; -exports.isLinux = () => - process.platform === 'linux'; +exports.isLinux = () => process.platform === 'linux'; -exports.isWindows = () => - process.platform === 'win32'; +exports.isWindows = () => process.platform === 'win32'; diff --git a/js/modules/privacy.js b/js/modules/privacy.js index e476a6b22..50a93dd9b 100644 --- a/js/modules/privacy.js +++ b/js/modules/privacy.js @@ -6,22 +6,20 @@ const path = require('path'); const { compose } = require('lodash/fp'); const { escapeRegExp } = require('lodash'); - const APP_ROOT_PATH = path.join(__dirname, '..', '..', '..'); const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g; const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g; const REDACTION_PLACEHOLDER = '[REDACTED]'; - // _redactPath :: Path -> String -> String -exports._redactPath = (filePath) => { +exports._redactPath = filePath => { if (!is.string(filePath)) { throw new TypeError("'filePath' must be a string"); } const filePathPattern = exports._pathToRegExp(filePath); - return (text) => { + return text => { if (!is.string(text)) { throw new TypeError("'text' must be a string"); } @@ -35,7 +33,7 @@ exports._redactPath = (filePath) => { }; // _pathToRegExp :: Path -> Maybe RegExp -exports._pathToRegExp = (filePath) => { +exports._pathToRegExp = filePath => { try { const pathWithNormalizedSlashes = filePath.replace(/\//g, '\\'); const pathWithEscapedSlashes = filePath.replace(/\\/g, '\\\\'); @@ -47,7 +45,9 @@ exports._pathToRegExp = (filePath) => { pathWithNormalizedSlashes, pathWithEscapedSlashes, urlEncodedPath, - ].map(escapeRegExp).join('|'); + ] + .map(escapeRegExp) + .join('|'); return new RegExp(patternString, 'g'); } catch (error) { return null; @@ -56,7 +56,7 @@ exports._pathToRegExp = (filePath) => { // Public API // redactPhoneNumbers :: String -> String -exports.redactPhoneNumbers = (text) => { +exports.redactPhoneNumbers = text => { if (!is.string(text)) { throw new TypeError("'text' must be a string"); } @@ -65,7 +65,7 @@ exports.redactPhoneNumbers = (text) => { }; // redactGroupIds :: String -> String -exports.redactGroupIds = (text) => { +exports.redactGroupIds = text => { if (!is.string(text)) { throw new TypeError("'text' must be a string"); } diff --git a/js/modules/settings.js b/js/modules/settings.js index 1d63b1e5b..334123877 100644 --- a/js/modules/settings.js +++ b/js/modules/settings.js @@ -1,6 +1,5 @@ const { isObject, isString } = require('lodash'); - const ITEMS_STORE_NAME = 'items'; const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex'; const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete'; @@ -37,8 +36,7 @@ exports._getItem = (connection, key) => { const itemsStore = transaction.objectStore(ITEMS_STORE_NAME); const request = itemsStore.get(key); return new Promise((resolve, reject) => { - request.onerror = event => - reject(event.target.error); + request.onerror = event => reject(event.target.error); request.onsuccess = event => resolve(event.target.result ? event.target.result.value : null); @@ -58,11 +56,9 @@ exports._setItem = (connection, key, value) => { const itemsStore = transaction.objectStore(ITEMS_STORE_NAME); const request = itemsStore.put({ id: key, value }, key); return new Promise((resolve, reject) => { - request.onerror = event => - reject(event.target.error); + request.onerror = event => reject(event.target.error); - request.onsuccess = () => - resolve(); + request.onsuccess = () => resolve(); }); }; @@ -79,10 +75,8 @@ exports._deleteItem = (connection, key) => { const itemsStore = transaction.objectStore(ITEMS_STORE_NAME); const request = itemsStore.delete(key); return new Promise((resolve, reject) => { - request.onerror = event => - reject(event.target.error); + request.onerror = event => reject(event.target.error); - request.onsuccess = () => - resolve(); + request.onsuccess = () => resolve(); }); }; diff --git a/js/modules/sleep.js b/js/modules/sleep.js index 4cc0fc61f..3ace89d62 100644 --- a/js/modules/sleep.js +++ b/js/modules/sleep.js @@ -1,4 +1,3 @@ /* global setTimeout */ -exports.sleep = ms => - new Promise(resolve => setTimeout(resolve, ms)); +exports.sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/js/modules/startup.js b/js/modules/startup.js index 06d950623..df3f07300 100644 --- a/js/modules/startup.js +++ b/js/modules/startup.js @@ -3,7 +3,6 @@ const is = require('@sindresorhus/is'); const Errors = require('./types/errors'); const Settings = require('./settings'); - exports.syncReadReceiptConfiguration = async ({ deviceId, sendRequestConfigurationSyncMessage, diff --git a/js/modules/string_to_array_buffer.js b/js/modules/string_to_array_buffer.js index 3ddd19d5b..60aa7e176 100644 --- a/js/modules/string_to_array_buffer.js +++ b/js/modules/string_to_array_buffer.js @@ -1,4 +1,4 @@ -exports.stringToArrayBuffer = (string) => { +exports.stringToArrayBuffer = string => { if (typeof string !== 'string') { throw new TypeError("'string' must be a string"); } diff --git a/js/modules/types/attachment.js b/js/modules/types/attachment.js index 7dba7b828..1a92cd8f0 100644 --- a/js/modules/types/attachment.js +++ b/js/modules/types/attachment.js @@ -2,9 +2,15 @@ const is = require('@sindresorhus/is'); const AttachmentTS = require('../../../ts/types/Attachment'); const MIME = require('../../../ts/types/MIME'); -const { arrayBufferToBlob, blobToArrayBuffer, dataURLToBlob } = require('blob-util'); +const { + arrayBufferToBlob, + blobToArrayBuffer, + dataURLToBlob, +} = require('blob-util'); const { autoOrientImage } = require('../auto_orient_image'); -const { migrateDataToFileSystem } = require('./attachment/migrate_data_to_file_system'); +const { + migrateDataToFileSystem, +} = require('./attachment/migrate_data_to_file_system'); // // Incoming message attachment fields // { @@ -30,7 +36,7 @@ const { migrateDataToFileSystem } = require('./attachment/migrate_data_to_file_s // Returns true if `rawAttachment` is a valid attachment based on our current schema. // Over time, we can expand this definition to become more narrow, e.g. require certain // fields, etc. -exports.isValid = (rawAttachment) => { +exports.isValid = rawAttachment => { // NOTE: We cannot use `_.isPlainObject` because `rawAttachment` is // deserialized by protobuf: if (!rawAttachment) { @@ -41,12 +47,15 @@ exports.isValid = (rawAttachment) => { }; // Upgrade steps -exports.autoOrientJPEG = async (attachment) => { +exports.autoOrientJPEG = async attachment => { if (!MIME.isJPEG(attachment.contentType)) { return attachment; } - const dataBlob = await arrayBufferToBlob(attachment.data, attachment.contentType); + const dataBlob = await arrayBufferToBlob( + attachment.data, + attachment.contentType + ); const newDataBlob = await dataURLToBlob(await autoOrientImage(dataBlob)); const newDataArrayBuffer = await blobToArrayBuffer(newDataBlob); @@ -76,7 +85,7 @@ const INVALID_CHARACTERS_PATTERN = new RegExp( // NOTE: Expose synchronous version to do property-based testing using `testcheck`, // which currently doesn’t support async testing: // https://github.com/leebyron/testcheck-js/issues/45 -exports._replaceUnicodeOrderOverridesSync = (attachment) => { +exports._replaceUnicodeOrderOverridesSync = attachment => { if (!is.string(attachment.fileName)) { return attachment; } @@ -95,9 +104,12 @@ exports._replaceUnicodeOrderOverridesSync = (attachment) => { exports.replaceUnicodeOrderOverrides = async attachment => exports._replaceUnicodeOrderOverridesSync(attachment); -exports.removeSchemaVersion = (attachment) => { +exports.removeSchemaVersion = attachment => { if (!exports.isValid(attachment)) { - console.log('Attachment.removeSchemaVersion: Invalid input attachment:', attachment); + console.log( + 'Attachment.removeSchemaVersion: Invalid input attachment:', + attachment + ); return attachment; } @@ -115,12 +127,12 @@ exports.hasData = attachment => // loadData :: (RelativePath -> IO (Promise ArrayBuffer)) // Attachment -> // IO (Promise Attachment) -exports.loadData = (readAttachmentData) => { +exports.loadData = readAttachmentData => { if (!is.function(readAttachmentData)) { throw new TypeError("'readAttachmentData' must be a function"); } - return async (attachment) => { + return async attachment => { if (!exports.isValid(attachment)) { throw new TypeError("'attachment' is not valid"); } @@ -142,12 +154,12 @@ exports.loadData = (readAttachmentData) => { // deleteData :: (RelativePath -> IO Unit) // Attachment -> // IO Unit -exports.deleteData = (deleteAttachmentData) => { +exports.deleteData = deleteAttachmentData => { if (!is.function(deleteAttachmentData)) { throw new TypeError("'deleteAttachmentData' must be a function"); } - return async (attachment) => { + return async attachment => { if (!exports.isValid(attachment)) { throw new TypeError("'attachment' is not valid"); } diff --git a/js/modules/types/attachment/migrate_data_to_file_system.js b/js/modules/types/attachment/migrate_data_to_file_system.js index debf2d2ca..e6b75d862 100644 --- a/js/modules/types/attachment/migrate_data_to_file_system.js +++ b/js/modules/types/attachment/migrate_data_to_file_system.js @@ -1,10 +1,4 @@ -const { - isArrayBuffer, - isFunction, - isUndefined, - omit, -} = require('lodash'); - +const { isArrayBuffer, isFunction, isUndefined, omit } = require('lodash'); // type Context :: { // writeNewAttachmentData :: ArrayBuffer -> Promise (IO Path) @@ -13,7 +7,10 @@ const { // migrateDataToFileSystem :: Attachment -> // Context -> // Promise Attachment -exports.migrateDataToFileSystem = async (attachment, { writeNewAttachmentData } = {}) => { +exports.migrateDataToFileSystem = async ( + attachment, + { writeNewAttachmentData } = {} +) => { if (!isFunction(writeNewAttachmentData)) { throw new TypeError("'writeNewAttachmentData' must be a function"); } @@ -28,15 +25,16 @@ exports.migrateDataToFileSystem = async (attachment, { writeNewAttachmentData } const isValidData = isArrayBuffer(data); if (!isValidData) { - throw new TypeError('Expected `attachment.data` to be an array buffer;' + - ` got: ${typeof attachment.data}`); + throw new TypeError( + 'Expected `attachment.data` to be an array buffer;' + + ` got: ${typeof attachment.data}` + ); } const path = await writeNewAttachmentData(data); - const attachmentWithoutData = omit( - Object.assign({}, attachment, { path }), - ['data'] - ); + const attachmentWithoutData = omit(Object.assign({}, attachment, { path }), [ + 'data', + ]); return attachmentWithoutData; }; diff --git a/js/modules/types/errors.js b/js/modules/types/errors.js index 0914eacc2..854bcc6ac 100644 --- a/js/modules/types/errors.js +++ b/js/modules/types/errors.js @@ -1,5 +1,5 @@ // toLogFormat :: Error -> String -exports.toLogFormat = (error) => { +exports.toLogFormat = error => { if (!error) { return error; } diff --git a/js/modules/types/message.js b/js/modules/types/message.js index 166252c14..79f3abf90 100644 --- a/js/modules/types/message.js +++ b/js/modules/types/message.js @@ -3,9 +3,9 @@ const { isFunction, isString, omit } = require('lodash'); const Attachment = require('./attachment'); const Errors = require('./errors'); const SchemaVersion = require('./schema_version'); -const { initializeAttachmentMetadata } = - require('../../../ts/types/message/initializeAttachmentMetadata'); - +const { + initializeAttachmentMetadata, +} = require('../../../ts/types/message/initializeAttachmentMetadata'); const GROUP = 'group'; const PRIVATE = 'private'; @@ -37,19 +37,17 @@ const INITIAL_SCHEMA_VERSION = 0; // how we do database migrations: exports.CURRENT_SCHEMA_VERSION = 5; - // Public API exports.GROUP = GROUP; exports.PRIVATE = PRIVATE; // Placeholder until we have stronger preconditions: -exports.isValid = () => - true; +exports.isValid = () => true; // Schema -exports.initializeSchemaVersion = (message) => { - const isInitialized = SchemaVersion.isValid(message.schemaVersion) && - message.schemaVersion >= 1; +exports.initializeSchemaVersion = message => { + const isInitialized = + SchemaVersion.isValid(message.schemaVersion) && message.schemaVersion >= 1; if (isInitialized) { return message; } @@ -59,27 +57,23 @@ exports.initializeSchemaVersion = (message) => { : 0; const hasAttachments = numAttachments > 0; if (!hasAttachments) { - return Object.assign( - {}, - message, - { schemaVersion: INITIAL_SCHEMA_VERSION } - ); + return Object.assign({}, message, { + schemaVersion: INITIAL_SCHEMA_VERSION, + }); } // All attachments should have the same schema version, so we just pick // the first one: const firstAttachment = message.attachments[0]; - const inheritedSchemaVersion = SchemaVersion.isValid(firstAttachment.schemaVersion) + const inheritedSchemaVersion = SchemaVersion.isValid( + firstAttachment.schemaVersion + ) ? firstAttachment.schemaVersion : INITIAL_SCHEMA_VERSION; - const messageWithInitialSchema = Object.assign( - {}, - message, - { - schemaVersion: inheritedSchemaVersion, - attachments: message.attachments.map(Attachment.removeSchemaVersion), - } - ); + const messageWithInitialSchema = Object.assign({}, message, { + schemaVersion: inheritedSchemaVersion, + attachments: message.attachments.map(Attachment.removeSchemaVersion), + }); return messageWithInitialSchema; }; @@ -98,7 +92,10 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => { return async (message, context) => { if (!exports.isValid(message)) { - console.log('Message._withSchemaVersion: Invalid input message:', message); + console.log( + 'Message._withSchemaVersion: Invalid input message:', + message + ); return message; } @@ -138,15 +135,10 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => { return message; } - return Object.assign( - {}, - upgradedMessage, - { schemaVersion } - ); + return Object.assign({}, upgradedMessage, { schemaVersion }); }; }; - // Public API // _mapAttachments :: (Attachment -> Promise Attachment) -> // (Message, Context) -> @@ -154,19 +146,24 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => { exports._mapAttachments = upgradeAttachment => async (message, context) => { const upgradeWithContext = attachment => upgradeAttachment(attachment, context); - const attachments = await Promise.all(message.attachments.map(upgradeWithContext)); + const attachments = await Promise.all( + message.attachments.map(upgradeWithContext) + ); return Object.assign({}, message, { attachments }); }; // _mapQuotedAttachments :: (QuotedAttachment -> Promise QuotedAttachment) -> // (Message, Context) -> // Promise Message -exports._mapQuotedAttachments = upgradeAttachment => async (message, context) => { +exports._mapQuotedAttachments = upgradeAttachment => async ( + message, + context +) => { if (!message.quote) { return message; } - const upgradeWithContext = async (attachment) => { + const upgradeWithContext = async attachment => { const { thumbnail } = attachment; if (!thumbnail) { return attachment; @@ -185,7 +182,9 @@ exports._mapQuotedAttachments = upgradeAttachment => async (message, context) => const quotedAttachments = (message.quote && message.quote.attachments) || []; - const attachments = await Promise.all(quotedAttachments.map(upgradeWithContext)); + const attachments = await Promise.all( + quotedAttachments.map(upgradeWithContext) + ); return Object.assign({}, message, { quote: Object.assign({}, message.quote, { attachments, @@ -193,8 +192,7 @@ exports._mapQuotedAttachments = upgradeAttachment => async (message, context) => }); }; -const toVersion0 = async message => - exports.initializeSchemaVersion(message); +const toVersion0 = async message => exports.initializeSchemaVersion(message); const toVersion1 = exports._withSchemaVersion( 1, @@ -241,25 +239,28 @@ exports.upgradeSchema = async (rawMessage, { writeNewAttachmentData } = {}) => { return message; }; -exports.createAttachmentLoader = (loadAttachmentData) => { +exports.createAttachmentLoader = loadAttachmentData => { if (!isFunction(loadAttachmentData)) { throw new TypeError('`loadAttachmentData` is required'); } - return async message => (Object.assign({}, message, { - attachments: await Promise.all(message.attachments.map(loadAttachmentData)), - })); + return async message => + Object.assign({}, message, { + attachments: await Promise.all( + message.attachments.map(loadAttachmentData) + ), + }); }; // createAttachmentDataWriter :: (RelativePath -> IO Unit) // Message -> // IO (Promise Message) -exports.createAttachmentDataWriter = (writeExistingAttachmentData) => { +exports.createAttachmentDataWriter = writeExistingAttachmentData => { if (!isFunction(writeExistingAttachmentData)) { throw new TypeError("'writeExistingAttachmentData' must be a function"); } - return async (rawMessage) => { + return async rawMessage => { if (!exports.isValid(rawMessage)) { throw new TypeError("'rawMessage' is not valid"); } @@ -282,17 +283,21 @@ exports.createAttachmentDataWriter = (writeExistingAttachmentData) => { return message; } - (attachments || []).forEach((attachment) => { + (attachments || []).forEach(attachment => { if (!Attachment.hasData(attachment)) { - throw new TypeError("'attachment.data' is required during message import"); + throw new TypeError( + "'attachment.data' is required during message import" + ); } if (!isString(attachment.path)) { - throw new TypeError("'attachment.path' is required during message import"); + throw new TypeError( + "'attachment.path' is required during message import" + ); } }); - const writeThumbnails = exports._mapQuotedAttachments(async (thumbnail) => { + const writeThumbnails = exports._mapQuotedAttachments(async thumbnail => { const { data, path } = thumbnail; // we want to be bulletproof to thumbnails without data @@ -315,10 +320,12 @@ exports.createAttachmentDataWriter = (writeExistingAttachmentData) => { {}, await writeThumbnails(message), { - attachments: await Promise.all((attachments || []).map(async (attachment) => { - await writeExistingAttachmentData(attachment); - return omit(attachment, ['data']); - })), + attachments: await Promise.all( + (attachments || []).map(async attachment => { + await writeExistingAttachmentData(attachment); + return omit(attachment, ['data']); + }) + ), } ); diff --git a/js/modules/types/schema_version.js b/js/modules/types/schema_version.js index 058a36a1e..957b1e4d8 100644 --- a/js/modules/types/schema_version.js +++ b/js/modules/types/schema_version.js @@ -1,5 +1,3 @@ const { isNumber } = require('lodash'); - -exports.isValid = value => - isNumber(value) && value >= 0; +exports.isValid = value => isNumber(value) && value >= 0; diff --git a/js/modules/types/settings.js b/js/modules/types/settings.js index ac5a60fe4..85fbd7add 100644 --- a/js/modules/types/settings.js +++ b/js/modules/types/settings.js @@ -1,4 +1,3 @@ const OS = require('../os'); -exports.isAudioNotificationSupported = () => - !OS.isLinux(); +exports.isAudioNotificationSupported = () => !OS.isLinux(); diff --git a/js/modules/views/initialization.js b/js/modules/views/initialization.js index 81ea45381..f543e81c9 100644 --- a/js/modules/views/initialization.js +++ b/js/modules/views/initialization.js @@ -2,7 +2,6 @@ /* global i18n: false */ - const OPTIMIZATION_MESSAGE_DISPLAY_THRESHOLD = 1000; // milliseconds const setMessage = () => { diff --git a/js/notifications.js b/js/notifications.js index 2c582e3b4..d7adf1684 100644 --- a/js/notifications.js +++ b/js/notifications.js @@ -2,140 +2,146 @@ * vim: ts=4:sw=4:expandtab */ -;(function() { - 'use strict'; - window.Whisper = window.Whisper || {}; - const { Settings } = window.Signal.Types; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; + const { Settings } = window.Signal.Types; - var SETTINGS = { - OFF : 'off', - COUNT : 'count', - NAME : 'name', - MESSAGE : 'message' - }; + var SETTINGS = { + OFF: 'off', + COUNT: 'count', + NAME: 'name', + MESSAGE: 'message', + }; - Whisper.Notifications = new (Backbone.Collection.extend({ - initialize: function() { - this.isEnabled = false; - this.on('add', this.update); - this.on('remove', this.onRemove); - }, - onClick: function(conversationId) { - var conversation = ConversationController.get(conversationId); - this.trigger('click', conversation); - }, - update: function() { - const {isEnabled} = this; - const isFocused = window.isFocused(); - const isAudioNotificationEnabled = storage.get('audio-notification') || false; - const isAudioNotificationSupported = Settings.isAudioNotificationSupported(); - const shouldPlayNotificationSound = isAudioNotificationSupported && - isAudioNotificationEnabled; - const numNotifications = this.length; - console.log( - 'Update notifications:', - {isFocused, isEnabled, numNotifications, shouldPlayNotificationSound} - ); + Whisper.Notifications = new (Backbone.Collection.extend({ + initialize: function() { + this.isEnabled = false; + this.on('add', this.update); + this.on('remove', this.onRemove); + }, + onClick: function(conversationId) { + var conversation = ConversationController.get(conversationId); + this.trigger('click', conversation); + }, + update: function() { + const { isEnabled } = this; + const isFocused = window.isFocused(); + const isAudioNotificationEnabled = + storage.get('audio-notification') || false; + const isAudioNotificationSupported = Settings.isAudioNotificationSupported(); + const shouldPlayNotificationSound = + isAudioNotificationSupported && isAudioNotificationEnabled; + const numNotifications = this.length; + console.log('Update notifications:', { + isFocused, + isEnabled, + numNotifications, + shouldPlayNotificationSound, + }); - if (!isEnabled) { - return; - } + if (!isEnabled) { + return; + } - const hasNotifications = numNotifications > 0; - if (!hasNotifications) { - return; - } + const hasNotifications = numNotifications > 0; + if (!hasNotifications) { + return; + } - const isNotificationOmitted = isFocused; - if (isNotificationOmitted) { - this.clear(); - return; - } + const isNotificationOmitted = isFocused; + if (isNotificationOmitted) { + this.clear(); + return; + } - var setting = storage.get('notification-setting') || 'message'; - if (setting === SETTINGS.OFF) { - return; - } + var setting = storage.get('notification-setting') || 'message'; + if (setting === SETTINGS.OFF) { + return; + } - window.drawAttention(); + window.drawAttention(); - var title; - var message; - var iconUrl; + var title; + var message; + var iconUrl; - // NOTE: i18n has more complex rules for pluralization than just - // distinguishing between zero (0) and other (non-zero), - // e.g. Russian: - // http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html - var newMessageCount = [ - numNotifications, - numNotifications === 1 ? i18n('newMessage') : i18n('newMessages') - ].join(' '); + // NOTE: i18n has more complex rules for pluralization than just + // distinguishing between zero (0) and other (non-zero), + // e.g. Russian: + // http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html + var newMessageCount = [ + numNotifications, + numNotifications === 1 ? i18n('newMessage') : i18n('newMessages'), + ].join(' '); - var last = this.last(); - switch (this.getSetting()) { - case SETTINGS.COUNT: - title = 'Signal'; - message = newMessageCount; - break; - case SETTINGS.NAME: - title = newMessageCount; - message = 'Most recent from ' + last.get('title'); - iconUrl = last.get('iconUrl'); - break; - case SETTINGS.MESSAGE: - if (numNotifications === 1) { - title = last.get('title'); - } else { - title = newMessageCount; - } - message = last.get('message'); - iconUrl = last.get('iconUrl'); - break; - } + var last = this.last(); + switch (this.getSetting()) { + case SETTINGS.COUNT: + title = 'Signal'; + message = newMessageCount; + break; + case SETTINGS.NAME: + title = newMessageCount; + message = 'Most recent from ' + last.get('title'); + iconUrl = last.get('iconUrl'); + break; + case SETTINGS.MESSAGE: + if (numNotifications === 1) { + title = last.get('title'); + } else { + title = newMessageCount; + } + message = last.get('message'); + iconUrl = last.get('iconUrl'); + break; + } - if (window.config.polyfillNotifications) { - window.nodeNotifier.notify({ - title: title, - message: message, - sound: false, - }); - window.nodeNotifier.on('click', function(notifierObject, options) { - last.get('conversationId'); - }); - } else { - var notification = new Notification(title, { - body : message, - icon : iconUrl, - tag : 'signal', - silent : !shouldPlayNotificationSound, - }); + if (window.config.polyfillNotifications) { + window.nodeNotifier.notify({ + title: title, + message: message, + sound: false, + }); + window.nodeNotifier.on('click', function(notifierObject, options) { + last.get('conversationId'); + }); + } else { + var notification = new Notification(title, { + body: message, + icon: iconUrl, + tag: 'signal', + silent: !shouldPlayNotificationSound, + }); - notification.onclick = this.onClick.bind(this, last.get('conversationId')); - } + notification.onclick = this.onClick.bind( + this, + last.get('conversationId') + ); + } - // We don't want to notify the user about these same messages again - this.clear(); - }, - getSetting: function() { - return storage.get('notification-setting') || SETTINGS.MESSAGE; - }, - onRemove: function() { - console.log('remove notification'); - }, - clear: function() { - console.log('remove all notifications'); - this.reset([]); - }, - enable: function() { - const needUpdate = !this.isEnabled; - this.isEnabled = true; - if (needUpdate) { - this.update(); - } - }, - disable: function() { - this.isEnabled = false; - }, - }))(); + // We don't want to notify the user about these same messages again + this.clear(); + }, + getSetting: function() { + return storage.get('notification-setting') || SETTINGS.MESSAGE; + }, + onRemove: function() { + console.log('remove notification'); + }, + clear: function() { + console.log('remove all notifications'); + this.reset([]); + }, + enable: function() { + const needUpdate = !this.isEnabled; + this.isEnabled = true; + if (needUpdate) { + this.update(); + } + }, + disable: function() { + this.isEnabled = false; + }, + }))(); })(); diff --git a/js/read_receipts.js b/js/read_receipts.js index db5363f97..0504d3907 100644 --- a/js/read_receipts.js +++ b/js/read_receipts.js @@ -1,79 +1,101 @@ /* * vim: ts=4:sw=4:expandtab */ -;(function() { - 'use strict'; - window.Whisper = window.Whisper || {}; - Whisper.ReadReceipts = new (Backbone.Collection.extend({ - forMessage: function(conversation, message) { - if (!message.isOutgoing()) { - return []; - } - var ids = []; - if (conversation.isPrivate()) { - ids = [conversation.id]; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; + Whisper.ReadReceipts = new (Backbone.Collection.extend({ + forMessage: function(conversation, message) { + if (!message.isOutgoing()) { + return []; + } + var ids = []; + if (conversation.isPrivate()) { + ids = [conversation.id]; + } else { + ids = conversation.get('members'); + } + var receipts = this.filter(function(receipt) { + return ( + receipt.get('timestamp') === message.get('sent_at') && + _.contains(ids, receipt.get('reader')) + ); + }); + if (receipts.length) { + console.log('Found early read receipts for message'); + this.remove(receipts); + } + return receipts; + }, + onReceipt: function(receipt) { + var messages = new Whisper.MessageCollection(); + return messages + .fetchSentAt(receipt.get('timestamp')) + .then(function() { + if (messages.length === 0) { + return; + } + var message = messages.find(function(message) { + return ( + message.isOutgoing() && + receipt.get('reader') === message.get('conversationId') + ); + }); + if (message) { + return message; + } + + var groups = new Whisper.GroupCollection(); + return groups.fetchGroups(receipt.get('reader')).then(function() { + var ids = groups.pluck('id'); + ids.push(receipt.get('reader')); + return messages.find(function(message) { + return ( + message.isOutgoing() && + _.contains(ids, message.get('conversationId')) + ); + }); + }); + }) + .then( + function(message) { + if (message) { + var read_by = message.get('read_by') || []; + read_by.push(receipt.get('reader')); + return new Promise( + function(resolve, reject) { + message.save({ read_by: read_by }).then( + function() { + // notify frontend listeners + var conversation = ConversationController.get( + message.get('conversationId') + ); + if (conversation) { + conversation.trigger('read', message); + } + + this.remove(receipt); + resolve(); + }.bind(this), + reject + ); + }.bind(this) + ); } else { - ids = conversation.get('members'); + console.log( + 'No message for read receipt', + receipt.get('reader'), + receipt.get('timestamp') + ); } - var receipts = this.filter(function(receipt) { - return receipt.get('timestamp') === message.get('sent_at') - && _.contains(ids, receipt.get('reader')); - }); - if (receipts.length) { - console.log('Found early read receipts for message'); - this.remove(receipts); - } - return receipts; - }, - onReceipt: function(receipt) { - var messages = new Whisper.MessageCollection(); - return messages.fetchSentAt(receipt.get('timestamp')).then(function() { - if (messages.length === 0) { return; } - var message = messages.find(function(message) { - return (message.isOutgoing() && receipt.get('reader') === message.get('conversationId')); - }); - if (message) { return message; } - - var groups = new Whisper.GroupCollection(); - return groups.fetchGroups(receipt.get('reader')).then(function() { - var ids = groups.pluck('id'); - ids.push(receipt.get('reader')); - return messages.find(function(message) { - return (message.isOutgoing() && - _.contains(ids, message.get('conversationId'))); - }); - }); - }).then(function(message) { - if (message) { - var read_by = message.get('read_by') || []; - read_by.push(receipt.get('reader')); - return new Promise(function(resolve, reject) { - message.save({ read_by: read_by }).then(function() { - // notify frontend listeners - var conversation = ConversationController.get( - message.get('conversationId') - ); - if (conversation) { - conversation.trigger('read', message); - } - - this.remove(receipt); - resolve(); - }.bind(this), reject); - }.bind(this)); - } else { - console.log( - 'No message for read receipt', - receipt.get('reader'), - receipt.get('timestamp') - ); - } - }.bind(this)).catch(function(error) { - console.log( - 'ReadReceipts.onReceipt error:', - error && error.stack ? error.stack : error - ); - }); - }, - }))(); + }.bind(this) + ) + .catch(function(error) { + console.log( + 'ReadReceipts.onReceipt error:', + error && error.stack ? error.stack : error + ); + }); + }, + }))(); })(); diff --git a/js/read_syncs.js b/js/read_syncs.js index fbb78d785..e846ce104 100644 --- a/js/read_syncs.js +++ b/js/read_syncs.js @@ -1,49 +1,57 @@ /* * vim: ts=4:sw=4:expandtab */ -;(function() { - 'use strict'; - window.Whisper = window.Whisper || {}; - Whisper.ReadSyncs = new (Backbone.Collection.extend({ - forMessage: function(message) { - var receipt = this.findWhere({ - sender: message.get('source'), - timestamp: message.get('sent_at') - }); - if (receipt) { - console.log('Found early read sync for message'); +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; + Whisper.ReadSyncs = new (Backbone.Collection.extend({ + forMessage: function(message) { + var receipt = this.findWhere({ + sender: message.get('source'), + timestamp: message.get('sent_at'), + }); + if (receipt) { + console.log('Found early read sync for message'); + this.remove(receipt); + return receipt; + } + }, + onReceipt: function(receipt) { + var messages = new Whisper.MessageCollection(); + return messages.fetchSentAt(receipt.get('timestamp')).then( + function() { + var message = messages.find(function(message) { + return ( + message.isIncoming() && + message.isUnread() && + message.get('source') === receipt.get('sender') + ); + }); + if (message) { + return message.markRead(receipt.get('read_at')).then( + function() { + this.notifyConversation(message); this.remove(receipt); - return receipt; - } - }, - onReceipt: function(receipt) { - var messages = new Whisper.MessageCollection(); - return messages.fetchSentAt(receipt.get('timestamp')).then(function() { - var message = messages.find(function(message) { - return (message.isIncoming() && message.isUnread() && - message.get('source') === receipt.get('sender')); - }); - if (message) { - return message.markRead(receipt.get('read_at')).then(function() { - this.notifyConversation(message); - this.remove(receipt); - }.bind(this)); - } else { - console.log( - 'No message for read sync', - receipt.get('sender'), receipt.get('timestamp') - ); - } - }.bind(this)); - }, - notifyConversation: function(message) { - var conversation = ConversationController.get({ - id: message.get('conversationId') - }); + }.bind(this) + ); + } else { + console.log( + 'No message for read sync', + receipt.get('sender'), + receipt.get('timestamp') + ); + } + }.bind(this) + ); + }, + notifyConversation: function(message) { + var conversation = ConversationController.get({ + id: message.get('conversationId'), + }); - if (conversation) { - conversation.onReadMessage(message); - } - }, - }))(); + if (conversation) { + conversation.onReadMessage(message); + } + }, + }))(); })(); diff --git a/js/registration.js b/js/registration.js index e482f2cb8..7398e742d 100644 --- a/js/registration.js +++ b/js/registration.js @@ -1,25 +1,27 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - Whisper.Registration = { - markEverDone: function() { - storage.put('chromiumRegistrationDoneEver', ''); - }, - markDone: function () { - this.markEverDone(); - storage.put('chromiumRegistrationDone', ''); - }, - isDone: function () { - return storage.get('chromiumRegistrationDone') === ''; - }, - everDone: function() { - return storage.get('chromiumRegistrationDoneEver') === '' || - storage.get('chromiumRegistrationDone') === ''; - }, - remove: function() { - storage.remove('chromiumRegistrationDone'); - } - }; -}()); +(function() { + 'use strict'; + Whisper.Registration = { + markEverDone: function() { + storage.put('chromiumRegistrationDoneEver', ''); + }, + markDone: function() { + this.markEverDone(); + storage.put('chromiumRegistrationDone', ''); + }, + isDone: function() { + return storage.get('chromiumRegistrationDone') === ''; + }, + everDone: function() { + return ( + storage.get('chromiumRegistrationDoneEver') === '' || + storage.get('chromiumRegistrationDone') === '' + ); + }, + remove: function() { + storage.remove('chromiumRegistrationDone'); + }, + }; +})(); diff --git a/js/reliable_trigger.js b/js/reliable_trigger.js index fb49ba47f..53ff46336 100644 --- a/js/reliable_trigger.js +++ b/js/reliable_trigger.js @@ -1,4 +1,4 @@ -(function () { +(function() { // Note: this is all the code required to customize Backbone's trigger() method to make // it resilient to exceptions thrown by event handlers. Indentation and code styles // were kept inline with the Backbone implementation for easier diffs. @@ -49,17 +49,26 @@ // triggering events. Tries to keep the usual cases speedy (most internal // Backbone events have 3 arguments). var triggerEvents = function(events, name, args) { - var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + var ev, + i = -1, + l = events.length, + a1 = args[0], + a2 = args[1], + a3 = args[2]; var logError = function(error) { - console.log('Model caught error triggering', name, 'event:', error && error.stack ? error.stack : error); + console.log( + 'Model caught error triggering', + name, + 'event:', + error && error.stack ? error.stack : error + ); }; switch (args.length) { case 0: while (++i < l) { try { (ev = events[i]).callback.call(ev.ctx); - } - catch (error) { + } catch (error) { logError(error); } } @@ -68,8 +77,7 @@ while (++i < l) { try { (ev = events[i]).callback.call(ev.ctx, a1); - } - catch (error) { + } catch (error) { logError(error); } } @@ -78,8 +86,7 @@ while (++i < l) { try { (ev = events[i]).callback.call(ev.ctx, a1, a2); - } - catch (error) { + } catch (error) { logError(error); } } @@ -88,8 +95,7 @@ while (++i < l) { try { (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); - } - catch (error) { + } catch (error) { logError(error); } } @@ -98,8 +104,7 @@ while (++i < l) { try { (ev = events[i]).callback.apply(ev.ctx, args); - } - catch (error) { + } catch (error) { logError(error); } } @@ -122,10 +127,5 @@ return this; } - Backbone.Model.prototype.trigger - = Backbone.View.prototype.trigger - = Backbone.Collection.prototype.trigger - = Backbone.Events.trigger - = trigger; + Backbone.Model.prototype.trigger = Backbone.View.prototype.trigger = Backbone.Collection.prototype.trigger = Backbone.Events.trigger = trigger; })(); - diff --git a/js/rotate_signed_prekey_listener.js b/js/rotate_signed_prekey_listener.js index ca73f512c..6802356da 100644 --- a/js/rotate_signed_prekey_listener.js +++ b/js/rotate_signed_prekey_listener.js @@ -2,83 +2,89 @@ * vim: ts=4:sw=4:expandtab */ -;(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; - var ROTATION_INTERVAL = 48 * 60 * 60 * 1000; - var timeout; - var scheduledTime; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; + var ROTATION_INTERVAL = 48 * 60 * 60 * 1000; + var timeout; + var scheduledTime; - function scheduleNextRotation() { - var now = Date.now(); - var nextTime = now + ROTATION_INTERVAL; - storage.put('nextSignedKeyRotationTime', nextTime); + function scheduleNextRotation() { + var now = Date.now(); + var nextTime = now + ROTATION_INTERVAL; + storage.put('nextSignedKeyRotationTime', nextTime); + } + + function run() { + console.log('Rotating signed prekey...'); + getAccountManager() + .rotateSignedPreKey() + .catch(function() { + console.log( + 'rotateSignedPrekey() failed. Trying again in five seconds' + ); + setTimeout(runWhenOnline, 5000); + }); + scheduleNextRotation(); + setTimeoutForNextRun(); + } + + function runWhenOnline() { + if (navigator.onLine) { + run(); + } else { + console.log( + 'We are offline; keys will be rotated when we are next online' + ); + var listener = function() { + window.removeEventListener('online', listener); + run(); + }; + window.addEventListener('online', listener); + } + } + + function setTimeoutForNextRun() { + var now = Date.now(); + var time = storage.get('nextSignedKeyRotationTime', now); + + if (scheduledTime !== time || !timeout) { + console.log( + 'Next signed key rotation scheduled for', + new Date(time).toISOString() + ); } - function run() { - console.log('Rotating signed prekey...'); - getAccountManager().rotateSignedPreKey().catch(function() { - console.log('rotateSignedPrekey() failed. Trying again in five seconds'); - setTimeout(runWhenOnline, 5000); - }); - scheduleNextRotation(); + scheduledTime = time; + var waitTime = time - now; + if (waitTime < 0) { + waitTime = 0; + } + + clearTimeout(timeout); + timeout = setTimeout(runWhenOnline, waitTime); + } + + var initComplete; + Whisper.RotateSignedPreKeyListener = { + init: function(events, newVersion) { + if (initComplete) { + console.log('Rotate signed prekey listener: Already initialized'); + return; + } + initComplete = true; + + if (newVersion) { + runWhenOnline(); + } else { setTimeoutForNextRun(); - } + } - function runWhenOnline() { - if (navigator.onLine) { - run(); - } else { - console.log('We are offline; keys will be rotated when we are next online'); - var listener = function() { - window.removeEventListener('online', listener); - run(); - }; - window.addEventListener('online', listener); + events.on('timetravel', function() { + if (Whisper.Registration.isDone()) { + setTimeoutForNextRun(); } - } - - function setTimeoutForNextRun() { - var now = Date.now(); - var time = storage.get('nextSignedKeyRotationTime', now); - - if (scheduledTime !== time || !timeout) { - console.log( - 'Next signed key rotation scheduled for', - new Date(time).toISOString() - ); - } - - scheduledTime = time; - var waitTime = time - now; - if (waitTime < 0) { - waitTime = 0; - } - - clearTimeout(timeout); - timeout = setTimeout(runWhenOnline, waitTime); - } - - var initComplete; - Whisper.RotateSignedPreKeyListener = { - init: function(events, newVersion) { - if (initComplete) { - console.log('Rotate signed prekey listener: Already initialized'); - return; - } - initComplete = true; - - if (newVersion) { - runWhenOnline(); - } else { - setTimeoutForNextRun(); - } - - events.on('timetravel', function() { - if (Whisper.Registration.isDone()) { - setTimeoutForNextRun(); - } - }); - } - }; -}()); + }); + }, + }; +})(); diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js index cfa3d057d..5276c80f4 100644 --- a/js/signal_protocol_store.js +++ b/js/signal_protocol_store.js @@ -1,885 +1,1037 @@ /* * vim: ts=4:sw=4:expandtab */ -;(function() { - 'use strict'; - var TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds - var Direction = { - SENDING: 1, - RECEIVING: 2, - }; +(function() { + 'use strict'; + var TIMESTAMP_THRESHOLD = 5 * 1000; // 5 seconds + var Direction = { + SENDING: 1, + RECEIVING: 2, + }; - var VerifiedStatus = { - DEFAULT: 0, - VERIFIED: 1, - UNVERIFIED: 2, - }; + var VerifiedStatus = { + DEFAULT: 0, + VERIFIED: 1, + UNVERIFIED: 2, + }; - function validateVerifiedStatus(status) { - if ( status === VerifiedStatus.DEFAULT - || status === VerifiedStatus.VERIFIED - || status === VerifiedStatus.UNVERIFIED) { - return true; + function validateVerifiedStatus(status) { + if ( + status === VerifiedStatus.DEFAULT || + status === VerifiedStatus.VERIFIED || + status === VerifiedStatus.UNVERIFIED + ) { + return true; + } + return false; + } + + var StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__; + var StaticArrayBufferProto = new ArrayBuffer().__proto__; + var StaticUint8ArrayProto = new Uint8Array().__proto__; + + function isStringable(thing) { + return ( + thing === Object(thing) && + (thing.__proto__ == StaticArrayBufferProto || + thing.__proto__ == StaticUint8ArrayProto || + thing.__proto__ == StaticByteBufferProto) + ); + } + function convertToArrayBuffer(thing) { + if (thing === undefined) { + return undefined; + } + if (thing === Object(thing)) { + if (thing.__proto__ == StaticArrayBufferProto) { + return thing; } + //TODO: Several more cases here... + } + + if (thing instanceof Array) { + // Assuming Uint16Array from curve25519 + var res = new ArrayBuffer(thing.length * 2); + var uint = new Uint16Array(res); + for (var i = 0; i < thing.length; i++) { + uint[i] = thing[i]; + } + return res; + } + + var str; + if (isStringable(thing)) { + str = stringObject(thing); + } else if (typeof thing == 'string') { + str = thing; + } else { + throw new Error( + 'Tried to convert a non-stringable thing of type ' + + typeof thing + + ' to an array buffer' + ); + } + var res = new ArrayBuffer(str.length); + var uint = new Uint8Array(res); + for (var i = 0; i < str.length; i++) { + uint[i] = str.charCodeAt(i); + } + return res; + } + + function equalArrayBuffers(ab1, ab2) { + if (!(ab1 instanceof ArrayBuffer && ab2 instanceof ArrayBuffer)) { return false; } - - var StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__; - var StaticArrayBufferProto = new ArrayBuffer().__proto__; - var StaticUint8ArrayProto = new Uint8Array().__proto__; - - function isStringable(thing) { - return (thing === Object(thing) && - (thing.__proto__ == StaticArrayBufferProto || - thing.__proto__ == StaticUint8ArrayProto || - thing.__proto__ == StaticByteBufferProto)); + if (ab1.byteLength !== ab2.byteLength) { + return false; } - function convertToArrayBuffer(thing) { - if (thing === undefined) { - return undefined; - } - if (thing === Object(thing)) { - if (thing.__proto__ == StaticArrayBufferProto) { - return thing; - } - //TODO: Several more cases here... - } - - if (thing instanceof Array) { - // Assuming Uint16Array from curve25519 - var res = new ArrayBuffer(thing.length * 2); - var uint = new Uint16Array(res); - for (var i = 0; i < thing.length; i++) { - uint[i] = thing[i]; - } - return res; - } - - var str; - if (isStringable(thing)) { - str = stringObject(thing); - } else if (typeof thing == "string") { - str = thing; - } else { - throw new Error("Tried to convert a non-stringable thing of type " + typeof thing + " to an array buffer"); - } - var res = new ArrayBuffer(str.length); - var uint = new Uint8Array(res); - for (var i = 0; i < str.length; i++) { - uint[i] = str.charCodeAt(i); - } - return res; + var result = 0; + var ta1 = new Uint8Array(ab1); + var ta2 = new Uint8Array(ab2); + for (var i = 0; i < ab1.byteLength; ++i) { + result = result | (ta1[i] ^ ta2[i]); } + return result === 0; + } - function equalArrayBuffers(ab1, ab2) { - if (!(ab1 instanceof ArrayBuffer && ab2 instanceof ArrayBuffer)) { - return false; - } - if (ab1.byteLength !== ab2.byteLength) { - return false; - } - var result = 0; - var ta1 = new Uint8Array(ab1); - var ta2 = new Uint8Array(ab2); - for (var i = 0; i < ab1.byteLength; ++i) { - result = result | ta1[i] ^ ta2[i]; - } - return result === 0; - } - - var Model = Backbone.Model.extend({ database: Whisper.Database }); - var PreKey = Model.extend({ storeName: 'preKeys' }); - var PreKeyCollection = Backbone.Collection.extend({ - storeName: 'preKeys', - database: Whisper.Database, - model: PreKey - }); - var SignedPreKey = Model.extend({ storeName: 'signedPreKeys' }); - var SignedPreKeyCollection = Backbone.Collection.extend({ - storeName: 'signedPreKeys', - database: Whisper.Database, - model: SignedPreKey - }); - var Session = Model.extend({ storeName: 'sessions' }); - var SessionCollection = Backbone.Collection.extend({ - storeName: 'sessions', - database: Whisper.Database, - model: Session, - fetchSessionsForNumber: function(number) { - return this.fetch({range: [number + '.1', number + '.' + ':']}); - } - }); - var Unprocessed = Model.extend({ storeName : 'unprocessed' }); - var UnprocessedCollection = Backbone.Collection.extend({ - storeName : 'unprocessed', - database : Whisper.Database, - model : Unprocessed, - comparator : 'timestamp' - }); - var IdentityRecord = Model.extend({ - storeName: 'identityKeys', - validAttributes: [ - 'id', - 'publicKey', - 'firstUse', - 'timestamp', - 'verified', - 'nonblockingApproval' - ], - validate: function(attrs, options) { - var attributeNames = _.keys(attrs); - var validAttributes = this.validAttributes; - var allValid = _.all(attributeNames, function(attributeName) { - return _.contains(validAttributes, attributeName); - }); - if (!allValid) { - return new Error("Invalid identity key attribute names"); - } - var allPresent = _.all(validAttributes, function(attributeName) { - return _.contains(attributeNames, attributeName); - }); - if (!allPresent) { - return new Error("Missing identity key attributes"); - } - - if (typeof attrs.id !== 'string') { - return new Error("Invalid identity key id"); - } - if (!(attrs.publicKey instanceof ArrayBuffer)) { - return new Error("Invalid identity key publicKey"); - } - if (typeof attrs.firstUse !== 'boolean') { - return new Error("Invalid identity key firstUse"); - } - if (typeof attrs.timestamp !== 'number' || !(attrs.timestamp >= 0)) { - return new Error("Invalid identity key timestamp"); - } - if (!validateVerifiedStatus(attrs.verified)) { - return new Error("Invalid identity key verified"); - } - if (typeof attrs.nonblockingApproval !== 'boolean') { - return new Error("Invalid identity key nonblockingApproval"); - } + var Model = Backbone.Model.extend({ database: Whisper.Database }); + var PreKey = Model.extend({ storeName: 'preKeys' }); + var PreKeyCollection = Backbone.Collection.extend({ + storeName: 'preKeys', + database: Whisper.Database, + model: PreKey, + }); + var SignedPreKey = Model.extend({ storeName: 'signedPreKeys' }); + var SignedPreKeyCollection = Backbone.Collection.extend({ + storeName: 'signedPreKeys', + database: Whisper.Database, + model: SignedPreKey, + }); + var Session = Model.extend({ storeName: 'sessions' }); + var SessionCollection = Backbone.Collection.extend({ + storeName: 'sessions', + database: Whisper.Database, + model: Session, + fetchSessionsForNumber: function(number) { + return this.fetch({ range: [number + '.1', number + '.' + ':'] }); + }, + }); + var Unprocessed = Model.extend({ storeName: 'unprocessed' }); + var UnprocessedCollection = Backbone.Collection.extend({ + storeName: 'unprocessed', + database: Whisper.Database, + model: Unprocessed, + comparator: 'timestamp', + }); + var IdentityRecord = Model.extend({ + storeName: 'identityKeys', + validAttributes: [ + 'id', + 'publicKey', + 'firstUse', + 'timestamp', + 'verified', + 'nonblockingApproval', + ], + validate: function(attrs, options) { + var attributeNames = _.keys(attrs); + var validAttributes = this.validAttributes; + var allValid = _.all(attributeNames, function(attributeName) { + return _.contains(validAttributes, attributeName); + }); + if (!allValid) { + return new Error('Invalid identity key attribute names'); + } + var allPresent = _.all(validAttributes, function(attributeName) { + return _.contains(attributeNames, attributeName); + }); + if (!allPresent) { + return new Error('Missing identity key attributes'); } - }); - var Group = Model.extend({ storeName: 'groups' }); - var Item = Model.extend({ storeName: 'items' }); - function SignalProtocolStore() {} + if (typeof attrs.id !== 'string') { + return new Error('Invalid identity key id'); + } + if (!(attrs.publicKey instanceof ArrayBuffer)) { + return new Error('Invalid identity key publicKey'); + } + if (typeof attrs.firstUse !== 'boolean') { + return new Error('Invalid identity key firstUse'); + } + if (typeof attrs.timestamp !== 'number' || !(attrs.timestamp >= 0)) { + return new Error('Invalid identity key timestamp'); + } + if (!validateVerifiedStatus(attrs.verified)) { + return new Error('Invalid identity key verified'); + } + if (typeof attrs.nonblockingApproval !== 'boolean') { + return new Error('Invalid identity key nonblockingApproval'); + } + }, + }); + var Group = Model.extend({ storeName: 'groups' }); + var Item = Model.extend({ storeName: 'items' }); - SignalProtocolStore.prototype = { - constructor: SignalProtocolStore, - getIdentityKeyPair: function() { - var item = new Item({id: 'identityKey'}); - return new Promise(function(resolve, reject) { - item.fetch().then(function() { - resolve(item.get('value')); - }, reject); - }); - }, - getLocalRegistrationId: function() { - var item = new Item({id: 'registrationId'}); - return new Promise(function(resolve, reject) { - item.fetch().then(function() { - resolve(item.get('value')); - }, reject); - }); - }, + function SignalProtocolStore() {} - /* Returns a prekeypair object or undefined */ - loadPreKey: function(keyId) { - var prekey = new PreKey({id: keyId}); - return new Promise(function(resolve) { - prekey.fetch().then(function() { - console.log('Successfully fetched prekey:', keyId); - resolve({ - pubKey: prekey.get('publicKey'), - privKey: prekey.get('privateKey'), - }); - }, function() { - console.log('Failed to fetch prekey:', keyId); - resolve(); - }); - }); - }, - storePreKey: function(keyId, keyPair) { - var prekey = new PreKey({ - id : keyId, - publicKey : keyPair.pubKey, - privateKey : keyPair.privKey - }); - return new Promise(function(resolve) { - prekey.save().always(function() { - resolve(); - }); - }); - }, - removePreKey: function(keyId) { - var prekey = new PreKey({id: keyId}); + SignalProtocolStore.prototype = { + constructor: SignalProtocolStore, + getIdentityKeyPair: function() { + var item = new Item({ id: 'identityKey' }); + return new Promise(function(resolve, reject) { + item.fetch().then(function() { + resolve(item.get('value')); + }, reject); + }); + }, + getLocalRegistrationId: function() { + var item = new Item({ id: 'registrationId' }); + return new Promise(function(resolve, reject) { + item.fetch().then(function() { + resolve(item.get('value')); + }, reject); + }); + }, - this.trigger('removePreKey'); + /* Returns a prekeypair object or undefined */ + loadPreKey: function(keyId) { + var prekey = new PreKey({ id: keyId }); + return new Promise(function(resolve) { + prekey.fetch().then( + function() { + console.log('Successfully fetched prekey:', keyId); + resolve({ + pubKey: prekey.get('publicKey'), + privKey: prekey.get('privateKey'), + }); + }, + function() { + console.log('Failed to fetch prekey:', keyId); + resolve(); + } + ); + }); + }, + storePreKey: function(keyId, keyPair) { + var prekey = new PreKey({ + id: keyId, + publicKey: keyPair.pubKey, + privateKey: keyPair.privKey, + }); + return new Promise(function(resolve) { + prekey.save().always(function() { + resolve(); + }); + }); + }, + removePreKey: function(keyId) { + var prekey = new PreKey({ id: keyId }); - return new Promise(function(resolve) { - var deferred = prekey.destroy(); - if (!deferred) { - return resolve(); - } + this.trigger('removePreKey'); - return deferred.then(resolve, function(error) { - console.log( - 'removePreKey error:', - error && error.stack ? error.stack : error - ); - resolve(); - }); - }); - }, - clearPreKeyStore: function() { - return new Promise(function(resolve) { - var preKeys = new PreKeyCollection(); - preKeys.sync('delete', preKeys, {}).always(resolve); - }); - }, - - /* Returns a signed keypair object or undefined */ - loadSignedPreKey: function(keyId) { - var prekey = new SignedPreKey({id: keyId}); - return new Promise(function(resolve) { - prekey.fetch().then(function() { - console.log('Successfully fetched signed prekey:', prekey.get('id')); - resolve({ - pubKey : prekey.get('publicKey'), - privKey : prekey.get('privateKey'), - created_at : prekey.get('created_at'), - keyId : prekey.get('id'), - confirmed : prekey.get('confirmed'), - }); - }).fail(function() { - console.log('Failed to fetch signed prekey:', keyId); - resolve(); - }); - }); - }, - loadSignedPreKeys: function() { - if (arguments.length > 0) { - return Promise.reject(new Error('loadSignedPreKeys takes no arguments')); - } - var signedPreKeys = new SignedPreKeyCollection(); - return new Promise(function(resolve) { - signedPreKeys.fetch().then(function() { - resolve(signedPreKeys.map(function(prekey) { - return { - pubKey : prekey.get('publicKey'), - privKey : prekey.get('privateKey'), - created_at : prekey.get('created_at'), - keyId : prekey.get('id'), - confirmed : prekey.get('confirmed'), - }; - })); - }); - }); - }, - storeSignedPreKey: function(keyId, keyPair, confirmed) { - var prekey = new SignedPreKey({ - id : keyId, - publicKey : keyPair.pubKey, - privateKey : keyPair.privKey, - created_at : Date.now(), - confirmed : Boolean(confirmed), - }); - return new Promise(function(resolve) { - prekey.save().always(function() { - resolve(); - }); - }); - }, - removeSignedPreKey: function(keyId) { - var prekey = new SignedPreKey({id: keyId}); - return new Promise(function(resolve, reject) { - var deferred = prekey.destroy(); - if (!deferred) { - return resolve(); - } - - deferred.then(resolve, reject); - }); - }, - clearSignedPreKeysStore: function() { - return new Promise(function(resolve) { - var signedPreKeys = new SignedPreKeyCollection(); - signedPreKeys.sync('delete', signedPreKeys, {}).always(resolve); - }); - }, - - loadSession: function(encodedNumber) { - if (encodedNumber === null || encodedNumber === undefined) { - throw new Error("Tried to get session for undefined/null number"); - } - return new Promise(function(resolve) { - var session = new Session({id: encodedNumber}); - session.fetch().always(function() { - resolve(session.get('record')); - }); - - }); - }, - storeSession: function(encodedNumber, record) { - if (encodedNumber === null || encodedNumber === undefined) { - throw new Error("Tried to put session for undefined/null number"); - } - return new Promise(function(resolve) { - var number = textsecure.utils.unencodeNumber(encodedNumber)[0]; - var deviceId = parseInt(textsecure.utils.unencodeNumber(encodedNumber)[1]); - - var session = new Session({id: encodedNumber}); - session.fetch().always(function() { - session.save({ - record: record, - deviceId: deviceId, - number: number - }).fail(function(e) { - console.log('Failed to save session', encodedNumber, e); - }).always(function() { - resolve(); - }); - }); - }); - }, - getDeviceIds: function(number) { - if (number === null || number === undefined) { - throw new Error("Tried to get device ids for undefined/null number"); - } - return new Promise(function(resolve) { - var sessions = new SessionCollection(); - sessions.fetchSessionsForNumber(number).always(function() { - resolve(sessions.pluck('deviceId')); - }); - }); - }, - removeSession: function(encodedNumber) { - console.log('deleting session for ', encodedNumber); - return new Promise(function(resolve) { - var session = new Session({id: encodedNumber}); - session.fetch().then(function() { - session.destroy().then(resolve); - }).fail(resolve); - }); - }, - removeAllSessions: function(number) { - if (number === null || number === undefined) { - throw new Error("Tried to remove sessions for undefined/null number"); - } - return new Promise(function(resolve, reject) { - var sessions = new SessionCollection(); - sessions.fetchSessionsForNumber(number).always(function() { - var promises = []; - while (sessions.length > 0) { - promises.push(new Promise(function(res, rej) { - sessions.pop().destroy().then(res, rej); - })); - } - Promise.all(promises).then(resolve, reject); - }); - }); - }, - archiveSiblingSessions: function(identifier) { - var address = libsignal.SignalProtocolAddress.fromString(identifier); - return this.getDeviceIds(address.getName()).then(function(deviceIds) { - var deviceIds = _.without(deviceIds, address.getDeviceId()); - return Promise.all(deviceIds.map(function(deviceId) { - var sibling = new libsignal.SignalProtocolAddress(address.getName(), deviceId); - console.log('closing session for', sibling.toString()); - var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, sibling); - return sessionCipher.closeOpenSessionForDevice(); - })); - }); - }, - archiveAllSessions: function(number) { - return this.getDeviceIds(number).then(function(deviceIds) { - return Promise.all(deviceIds.map(function(deviceId) { - var address = new libsignal.SignalProtocolAddress(number, deviceId); - console.log('closing session for', address.toString()); - var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address); - return sessionCipher.closeOpenSessionForDevice(); - })); - }); - }, - clearSessionStore: function() { - return new Promise(function(resolve) { - var sessions = new SessionCollection(); - sessions.sync('delete', sessions, {}).always(resolve); - }); - }, - isTrustedIdentity: function(identifier, publicKey, direction) { - if (identifier === null || identifier === undefined) { - throw new Error("Tried to get identity key for undefined/null key"); - } - var number = textsecure.utils.unencodeNumber(identifier)[0]; - var isOurNumber = number === textsecure.storage.user.getNumber(); - var identityRecord = new IdentityRecord({id: number}); - return new Promise(function(resolve) { - identityRecord.fetch().always(resolve); - }).then(function() { - var existing = identityRecord.get('publicKey'); - - if (isOurNumber) { - return equalArrayBuffers(existing, publicKey); - } - - switch(direction) { - case Direction.SENDING: return this.isTrustedForSending(publicKey, identityRecord); - case Direction.RECEIVING: return true; - default: throw new Error("Unknown direction: " + direction); - } - }.bind(this)); - }, - isTrustedForSending: function(publicKey, identityRecord) { - var existing = identityRecord.get('publicKey'); - - if (!existing) { - console.log("isTrustedForSending: Nothing here, returning true..."); - return true; - } - if (!equalArrayBuffers(existing, publicKey)) { - console.log("isTrustedForSending: Identity keys don't match..."); - return false; - } - if (identityRecord.get('verified') === VerifiedStatus.UNVERIFIED) { - console.log("Needs unverified approval!"); - return false; - } - if (this.isNonBlockingApprovalRequired(identityRecord)) { - console.log("isTrustedForSending: Needs non-blocking approval!"); - return false; - } - - return true; - }, - loadIdentityKey: function(identifier) { - if (identifier === null || identifier === undefined) { - throw new Error("Tried to get identity key for undefined/null key"); - } - var number = textsecure.utils.unencodeNumber(identifier)[0]; - return new Promise(function(resolve) { - var identityRecord = new IdentityRecord({id: number}); - identityRecord.fetch().always(function() { - resolve(identityRecord.get('publicKey')); - }); - }); - }, - saveIdentity: function(identifier, publicKey, nonblockingApproval) { - if (identifier === null || identifier === undefined) { - throw new Error("Tried to put identity key for undefined/null key"); - } - if (!(publicKey instanceof ArrayBuffer)) { - publicKey = convertToArrayBuffer(publicKey); - } - if (typeof nonblockingApproval !== 'boolean') { - nonblockingApproval = false; - } - var number = textsecure.utils.unencodeNumber(identifier)[0]; - return new Promise(function(resolve, reject) { - var identityRecord = new IdentityRecord({id: number}); - identityRecord.fetch().always(function() { - var oldpublicKey = identityRecord.get('publicKey'); - if (!oldpublicKey) { - // Lookup failed, or the current key was removed, so save this one. - console.log("Saving new identity..."); - identityRecord.save({ - publicKey : publicKey, - firstUse : true, - timestamp : Date.now(), - verified : VerifiedStatus.DEFAULT, - nonblockingApproval : nonblockingApproval, - }).then(function() { - resolve(false); - }, reject); - } else if (!equalArrayBuffers(oldpublicKey, publicKey)) { - console.log("Replacing existing identity..."); - var previousStatus = identityRecord.get('verified'); - var verifiedStatus; - if (previousStatus === VerifiedStatus.VERIFIED - || previousStatus === VerifiedStatus.UNVERIFIED) { - verifiedStatus = VerifiedStatus.UNVERIFIED; - } else { - verifiedStatus = VerifiedStatus.DEFAULT; - } - identityRecord.save({ - publicKey : publicKey, - firstUse : false, - timestamp : Date.now(), - verified : verifiedStatus, - nonblockingApproval : nonblockingApproval, - }).then(function() { - this.trigger('keychange', number); - this.archiveSiblingSessions(identifier).then(function() { - resolve(true); - }, reject); - }.bind(this), reject); - } else if (this.isNonBlockingApprovalRequired(identityRecord)) { - console.log("Setting approval status..."); - identityRecord.save({ - nonblockingApproval : nonblockingApproval, - }).then(function() { - resolve(false); - }, reject); - } else { - resolve(false); - } - }.bind(this)); - }.bind(this)); - }, - isNonBlockingApprovalRequired: function(identityRecord) { - return (!identityRecord.get('firstUse') - && Date.now() - identityRecord.get('timestamp') < TIMESTAMP_THRESHOLD - && !identityRecord.get('nonblockingApproval')); - }, - saveIdentityWithAttributes: function(identifier, attributes) { - if (identifier === null || identifier === undefined) { - throw new Error("Tried to put identity key for undefined/null key"); - } - var number = textsecure.utils.unencodeNumber(identifier)[0]; - return new Promise(function(resolve, reject) { - var identityRecord = new IdentityRecord({id: number}); - identityRecord.set(attributes); - if (identityRecord.isValid()) { // false if invalid attributes - identityRecord.save().then(resolve); - } else { - reject(identityRecord.validationError); - } - }); - }, - setApproval: function(identifier, nonblockingApproval) { - if (identifier === null || identifier === undefined) { - throw new Error("Tried to set approval for undefined/null identifier"); - } - if (typeof nonblockingApproval !== 'boolean') { - throw new Error("Invalid approval status"); - } - var number = textsecure.utils.unencodeNumber(identifier)[0]; - return new Promise(function(resolve, reject) { - var identityRecord = new IdentityRecord({id: number}); - identityRecord.fetch().then(function() { - identityRecord.save({ - nonblockingApproval: nonblockingApproval - }).then(function() { - resolve(); - }, function() { // catch - reject(new Error("No identity record for " + number)); - }); - }); - }); - }, - setVerified: function(identifier, verifiedStatus, publicKey) { - if (identifier === null || identifier === undefined) { - throw new Error("Tried to set verified for undefined/null key"); - } - if (!validateVerifiedStatus(verifiedStatus)) { - throw new Error("Invalid verified status"); - } - if (arguments.length > 2 && !(publicKey instanceof ArrayBuffer)) { - throw new Error("Invalid public key"); - } - return new Promise(function(resolve, reject) { - var identityRecord = new IdentityRecord({id: identifier}); - identityRecord.fetch().then(function() { - if (!publicKey || equalArrayBuffers(identityRecord.get('publicKey'), publicKey)) { - identityRecord.set({ verified: verifiedStatus }); - - if (identityRecord.isValid()) { - identityRecord.save({ - }).then(function() { - resolve(); - }, reject); - } else { - reject(identityRecord.validationError); - } - } else { - console.log("No identity record for specified publicKey"); - resolve(); - } - }, function() { // catch - reject(new Error("No identity record for " + identifier)); - }); - }); - }, - getVerified: function(identifier) { - if (identifier === null || identifier === undefined) { - throw new Error("Tried to set verified for undefined/null key"); - } - return new Promise(function(resolve, reject) { - var identityRecord = new IdentityRecord({id: identifier}); - identityRecord.fetch().then(function() { - var verifiedStatus = identityRecord.get('verified'); - if (validateVerifiedStatus(verifiedStatus)) { - resolve(verifiedStatus); - } - else { - resolve(VerifiedStatus.DEFAULT); - } - }, function() { // catch - reject(new Error("No identity record for " + identifier)); - }); - }); - }, - // Resolves to true if a new identity key was saved - processContactSyncVerificationState: function(identifier, verifiedStatus, publicKey) { - if (verifiedStatus === VerifiedStatus.UNVERIFIED) { - return this.processUnverifiedMessage(identifier, verifiedStatus, publicKey); - } else { - return this.processVerifiedMessage(identifier, verifiedStatus, publicKey); - } - }, - // This function encapsulates the non-Java behavior, since the mobile apps don't - // currently receive contact syncs and therefore will see a verify sync with - // UNVERIFIED status - processUnverifiedMessage: function(identifier, verifiedStatus, publicKey) { - if (identifier === null || identifier === undefined) { - throw new Error("Tried to set verified for undefined/null key"); - } - if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) { - throw new Error("Invalid public key"); - } - return new Promise(function(resolve, reject) { - var identityRecord = new IdentityRecord({id: identifier}); - var isPresent = false; - var isEqual = false; - identityRecord.fetch().then(function() { - isPresent = true; - if (publicKey) { - isEqual = equalArrayBuffers(publicKey, identityRecord.get('publicKey')); - } - }).always(function() { - if (isPresent - && isEqual - && identityRecord.get('verified') !== VerifiedStatus.UNVERIFIED) { - - return textsecure.storage.protocol.setVerified( - identifier, verifiedStatus, publicKey - ).then(resolve, reject); - } - - if (!isPresent || !isEqual) { - return textsecure.storage.protocol.saveIdentityWithAttributes(identifier, { - publicKey : publicKey, - verified : verifiedStatus, - firstUse : false, - timestamp : Date.now(), - nonblockingApproval : true - }).then(function() { - if (isPresent && !isEqual) { - this.trigger('keychange', identifier); - return this.archiveAllSessions(identifier).then(function() { - // true signifies that we overwrote a previous key with a new one - return resolve(true); - }, reject); - } - - return resolve(); - }.bind(this), reject); - } - - // The situation which could get us here is: - // 1. had a previous key - // 2. new key is the same - // 3. desired new status is same as what we had before - return resolve(); - }.bind(this)); - }.bind(this)); - }, - // This matches the Java method as of - // https://github.com/signalapp/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188 - processVerifiedMessage: function(identifier, verifiedStatus, publicKey) { - if (identifier === null || identifier === undefined) { - throw new Error("Tried to set verified for undefined/null key"); - } - if (!validateVerifiedStatus(verifiedStatus)) { - throw new Error("Invalid verified status"); - } - if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) { - throw new Error("Invalid public key"); - } - return new Promise(function(resolve, reject) { - var identityRecord = new IdentityRecord({id: identifier}); - var isPresent = false; - var isEqual = false; - identityRecord.fetch().then(function() { - isPresent = true; - if (publicKey) { - isEqual = equalArrayBuffers(publicKey, identityRecord.get('publicKey')); - } - }).always(function() { - if (!isPresent && verifiedStatus === VerifiedStatus.DEFAULT) { - console.log('No existing record for default status'); - return resolve(); - } - - if (isPresent && isEqual - && identityRecord.get('verified') !== VerifiedStatus.DEFAULT - && verifiedStatus === VerifiedStatus.DEFAULT) { - - return textsecure.storage.protocol.setVerified( - identifier, verifiedStatus, publicKey - ).then(resolve, reject); - } - - if (verifiedStatus === VerifiedStatus.VERIFIED - && (!isPresent - || (isPresent && !isEqual) - || (isPresent && identityRecord.get('verified') !== VerifiedStatus.VERIFIED))) { - - return textsecure.storage.protocol.saveIdentityWithAttributes(identifier, { - publicKey : publicKey, - verified : verifiedStatus, - firstUse : false, - timestamp : Date.now(), - nonblockingApproval : true - }).then(function() { - if (isPresent && !isEqual) { - this.trigger('keychange', identifier); - return this.archiveAllSessions(identifier).then(function() { - // true signifies that we overwrote a previous key with a new one - return resolve(true); - }, reject); - } - - return resolve(); - }.bind(this), reject); - } - - // We get here if we got a new key and the status is DEFAULT. If the - // message is out of date, we don't want to lose whatever more-secure - // state we had before. - return resolve(); - }.bind(this)); - }.bind(this)); - }, - isUntrusted: function(identifier) { - if (identifier === null || identifier === undefined) { - throw new Error("Tried to set verified for undefined/null key"); - } - return new Promise(function(resolve, reject) { - var identityRecord = new IdentityRecord({id: identifier}); - identityRecord.fetch().then(function() { - if (Date.now() - identityRecord.get('timestamp') < TIMESTAMP_THRESHOLD - && !identityRecord.get('nonblockingApproval') - && !identityRecord.get('firstUse')) { - resolve(true); - } else { - resolve(false); - } - }, function() { // catch - reject(new Error("No identity record for " + identifier)); - }); - }); - }, - removeIdentityKey: function(number) { - return new Promise(function(resolve, reject) { - var identityRecord = new IdentityRecord({id: number}); - identityRecord.fetch().then(function() { - identityRecord.destroy(); - }).fail(function() { - reject(new Error("Tried to remove identity for unknown number")); - }); - resolve(textsecure.storage.protocol.removeAllSessions(number)); - }); - }, - - // Groups - getGroup: function(groupId) { - if (groupId === null || groupId === undefined) { - throw new Error("Tried to get group for undefined/null id"); - } - return new Promise(function(resolve) { - var group = new Group({id: groupId}); - group.fetch().always(function() { - resolve(group.get('data')); - }); - }); - }, - putGroup: function(groupId, group) { - if (groupId === null || groupId === undefined) { - throw new Error("Tried to put group key for undefined/null id"); - } - if (group === null || group === undefined) { - throw new Error("Tried to put undefined/null group object"); - } - var group = new Group({id: groupId, data: group}); - return new Promise(function(resolve) { - group.save().always(resolve); - }); - }, - removeGroup: function(groupId) { - if (groupId === null || groupId === undefined) { - throw new Error("Tried to remove group key for undefined/null id"); - } - return new Promise(function(resolve) { - var group = new Group({id: groupId}); - group.destroy().always(resolve); - }); - }, - - // Not yet processed messages - for resiliency - getAllUnprocessed: function() { - var collection; - return new Promise(function(resolve, reject) { - collection = new UnprocessedCollection(); - return collection.fetch().then(resolve, reject); - }).then(function() { - // Return a plain array of plain objects - return collection.map('attributes'); - }); - }, - addUnprocessed: function(data) { - return new Promise(function(resolve, reject) { - var unprocessed = new Unprocessed(data); - return unprocessed.save().then(resolve, reject); - }); - }, - updateUnprocessed: function(id, updates) { - return new Promise(function(resolve, reject) { - var unprocessed = new Unprocessed({ - id: id - }); - return unprocessed.fetch().then(function() { - return unprocessed.save(updates).then(resolve, reject); - }, reject); - }.bind(this)); - }, - removeUnprocessed: function(id) { - return new Promise(function(resolve, reject) { - var unprocessed = new Unprocessed({ - id: id - }); - return unprocessed.destroy().then(resolve, reject); - }.bind(this)); - }, - removeAllData: function() { - // First the in-memory caches: - window.storage.reset(); // items store - ConversationController.reset(); // conversations store - - // Then, the entire database: - return Whisper.Database.clear(); - }, - removeAllConfiguration: function() { - // First the in-memory cache for the items store: - window.storage.reset(); - - // Then anything in the database that isn't a message/conversation/group: - return Whisper.Database.clearStores([ - 'items', - 'identityKeys', - 'sessions', - 'signedPreKeys', - 'preKeys', - 'unprocessed', - ]); + return new Promise(function(resolve) { + var deferred = prekey.destroy(); + if (!deferred) { + return resolve(); } - }; - _.extend(SignalProtocolStore.prototype, Backbone.Events); - window.SignalProtocolStore = SignalProtocolStore; - window.SignalProtocolStore.prototype.Direction = Direction; - window.SignalProtocolStore.prototype.VerifiedStatus = VerifiedStatus; + return deferred.then(resolve, function(error) { + console.log( + 'removePreKey error:', + error && error.stack ? error.stack : error + ); + resolve(); + }); + }); + }, + clearPreKeyStore: function() { + return new Promise(function(resolve) { + var preKeys = new PreKeyCollection(); + preKeys.sync('delete', preKeys, {}).always(resolve); + }); + }, + + /* Returns a signed keypair object or undefined */ + loadSignedPreKey: function(keyId) { + var prekey = new SignedPreKey({ id: keyId }); + return new Promise(function(resolve) { + prekey + .fetch() + .then(function() { + console.log( + 'Successfully fetched signed prekey:', + prekey.get('id') + ); + resolve({ + pubKey: prekey.get('publicKey'), + privKey: prekey.get('privateKey'), + created_at: prekey.get('created_at'), + keyId: prekey.get('id'), + confirmed: prekey.get('confirmed'), + }); + }) + .fail(function() { + console.log('Failed to fetch signed prekey:', keyId); + resolve(); + }); + }); + }, + loadSignedPreKeys: function() { + if (arguments.length > 0) { + return Promise.reject( + new Error('loadSignedPreKeys takes no arguments') + ); + } + var signedPreKeys = new SignedPreKeyCollection(); + return new Promise(function(resolve) { + signedPreKeys.fetch().then(function() { + resolve( + signedPreKeys.map(function(prekey) { + return { + pubKey: prekey.get('publicKey'), + privKey: prekey.get('privateKey'), + created_at: prekey.get('created_at'), + keyId: prekey.get('id'), + confirmed: prekey.get('confirmed'), + }; + }) + ); + }); + }); + }, + storeSignedPreKey: function(keyId, keyPair, confirmed) { + var prekey = new SignedPreKey({ + id: keyId, + publicKey: keyPair.pubKey, + privateKey: keyPair.privKey, + created_at: Date.now(), + confirmed: Boolean(confirmed), + }); + return new Promise(function(resolve) { + prekey.save().always(function() { + resolve(); + }); + }); + }, + removeSignedPreKey: function(keyId) { + var prekey = new SignedPreKey({ id: keyId }); + return new Promise(function(resolve, reject) { + var deferred = prekey.destroy(); + if (!deferred) { + return resolve(); + } + + deferred.then(resolve, reject); + }); + }, + clearSignedPreKeysStore: function() { + return new Promise(function(resolve) { + var signedPreKeys = new SignedPreKeyCollection(); + signedPreKeys.sync('delete', signedPreKeys, {}).always(resolve); + }); + }, + + loadSession: function(encodedNumber) { + if (encodedNumber === null || encodedNumber === undefined) { + throw new Error('Tried to get session for undefined/null number'); + } + return new Promise(function(resolve) { + var session = new Session({ id: encodedNumber }); + session.fetch().always(function() { + resolve(session.get('record')); + }); + }); + }, + storeSession: function(encodedNumber, record) { + if (encodedNumber === null || encodedNumber === undefined) { + throw new Error('Tried to put session for undefined/null number'); + } + return new Promise(function(resolve) { + var number = textsecure.utils.unencodeNumber(encodedNumber)[0]; + var deviceId = parseInt( + textsecure.utils.unencodeNumber(encodedNumber)[1] + ); + + var session = new Session({ id: encodedNumber }); + session.fetch().always(function() { + session + .save({ + record: record, + deviceId: deviceId, + number: number, + }) + .fail(function(e) { + console.log('Failed to save session', encodedNumber, e); + }) + .always(function() { + resolve(); + }); + }); + }); + }, + getDeviceIds: function(number) { + if (number === null || number === undefined) { + throw new Error('Tried to get device ids for undefined/null number'); + } + return new Promise(function(resolve) { + var sessions = new SessionCollection(); + sessions.fetchSessionsForNumber(number).always(function() { + resolve(sessions.pluck('deviceId')); + }); + }); + }, + removeSession: function(encodedNumber) { + console.log('deleting session for ', encodedNumber); + return new Promise(function(resolve) { + var session = new Session({ id: encodedNumber }); + session + .fetch() + .then(function() { + session.destroy().then(resolve); + }) + .fail(resolve); + }); + }, + removeAllSessions: function(number) { + if (number === null || number === undefined) { + throw new Error('Tried to remove sessions for undefined/null number'); + } + return new Promise(function(resolve, reject) { + var sessions = new SessionCollection(); + sessions.fetchSessionsForNumber(number).always(function() { + var promises = []; + while (sessions.length > 0) { + promises.push( + new Promise(function(res, rej) { + sessions + .pop() + .destroy() + .then(res, rej); + }) + ); + } + Promise.all(promises).then(resolve, reject); + }); + }); + }, + archiveSiblingSessions: function(identifier) { + var address = libsignal.SignalProtocolAddress.fromString(identifier); + return this.getDeviceIds(address.getName()).then(function(deviceIds) { + var deviceIds = _.without(deviceIds, address.getDeviceId()); + return Promise.all( + deviceIds.map(function(deviceId) { + var sibling = new libsignal.SignalProtocolAddress( + address.getName(), + deviceId + ); + console.log('closing session for', sibling.toString()); + var sessionCipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + sibling + ); + return sessionCipher.closeOpenSessionForDevice(); + }) + ); + }); + }, + archiveAllSessions: function(number) { + return this.getDeviceIds(number).then(function(deviceIds) { + return Promise.all( + deviceIds.map(function(deviceId) { + var address = new libsignal.SignalProtocolAddress(number, deviceId); + console.log('closing session for', address.toString()); + var sessionCipher = new libsignal.SessionCipher( + textsecure.storage.protocol, + address + ); + return sessionCipher.closeOpenSessionForDevice(); + }) + ); + }); + }, + clearSessionStore: function() { + return new Promise(function(resolve) { + var sessions = new SessionCollection(); + sessions.sync('delete', sessions, {}).always(resolve); + }); + }, + isTrustedIdentity: function(identifier, publicKey, direction) { + if (identifier === null || identifier === undefined) { + throw new Error('Tried to get identity key for undefined/null key'); + } + var number = textsecure.utils.unencodeNumber(identifier)[0]; + var isOurNumber = number === textsecure.storage.user.getNumber(); + var identityRecord = new IdentityRecord({ id: number }); + return new Promise(function(resolve) { + identityRecord.fetch().always(resolve); + }).then( + function() { + var existing = identityRecord.get('publicKey'); + + if (isOurNumber) { + return equalArrayBuffers(existing, publicKey); + } + + switch (direction) { + case Direction.SENDING: + return this.isTrustedForSending(publicKey, identityRecord); + case Direction.RECEIVING: + return true; + default: + throw new Error('Unknown direction: ' + direction); + } + }.bind(this) + ); + }, + isTrustedForSending: function(publicKey, identityRecord) { + var existing = identityRecord.get('publicKey'); + + if (!existing) { + console.log('isTrustedForSending: Nothing here, returning true...'); + return true; + } + if (!equalArrayBuffers(existing, publicKey)) { + console.log("isTrustedForSending: Identity keys don't match..."); + return false; + } + if (identityRecord.get('verified') === VerifiedStatus.UNVERIFIED) { + console.log('Needs unverified approval!'); + return false; + } + if (this.isNonBlockingApprovalRequired(identityRecord)) { + console.log('isTrustedForSending: Needs non-blocking approval!'); + return false; + } + + return true; + }, + loadIdentityKey: function(identifier) { + if (identifier === null || identifier === undefined) { + throw new Error('Tried to get identity key for undefined/null key'); + } + var number = textsecure.utils.unencodeNumber(identifier)[0]; + return new Promise(function(resolve) { + var identityRecord = new IdentityRecord({ id: number }); + identityRecord.fetch().always(function() { + resolve(identityRecord.get('publicKey')); + }); + }); + }, + saveIdentity: function(identifier, publicKey, nonblockingApproval) { + if (identifier === null || identifier === undefined) { + throw new Error('Tried to put identity key for undefined/null key'); + } + if (!(publicKey instanceof ArrayBuffer)) { + publicKey = convertToArrayBuffer(publicKey); + } + if (typeof nonblockingApproval !== 'boolean') { + nonblockingApproval = false; + } + var number = textsecure.utils.unencodeNumber(identifier)[0]; + return new Promise( + function(resolve, reject) { + var identityRecord = new IdentityRecord({ id: number }); + identityRecord.fetch().always( + function() { + var oldpublicKey = identityRecord.get('publicKey'); + if (!oldpublicKey) { + // Lookup failed, or the current key was removed, so save this one. + console.log('Saving new identity...'); + identityRecord + .save({ + publicKey: publicKey, + firstUse: true, + timestamp: Date.now(), + verified: VerifiedStatus.DEFAULT, + nonblockingApproval: nonblockingApproval, + }) + .then(function() { + resolve(false); + }, reject); + } else if (!equalArrayBuffers(oldpublicKey, publicKey)) { + console.log('Replacing existing identity...'); + var previousStatus = identityRecord.get('verified'); + var verifiedStatus; + if ( + previousStatus === VerifiedStatus.VERIFIED || + previousStatus === VerifiedStatus.UNVERIFIED + ) { + verifiedStatus = VerifiedStatus.UNVERIFIED; + } else { + verifiedStatus = VerifiedStatus.DEFAULT; + } + identityRecord + .save({ + publicKey: publicKey, + firstUse: false, + timestamp: Date.now(), + verified: verifiedStatus, + nonblockingApproval: nonblockingApproval, + }) + .then( + function() { + this.trigger('keychange', number); + this.archiveSiblingSessions(identifier).then(function() { + resolve(true); + }, reject); + }.bind(this), + reject + ); + } else if (this.isNonBlockingApprovalRequired(identityRecord)) { + console.log('Setting approval status...'); + identityRecord + .save({ + nonblockingApproval: nonblockingApproval, + }) + .then(function() { + resolve(false); + }, reject); + } else { + resolve(false); + } + }.bind(this) + ); + }.bind(this) + ); + }, + isNonBlockingApprovalRequired: function(identityRecord) { + return ( + !identityRecord.get('firstUse') && + Date.now() - identityRecord.get('timestamp') < TIMESTAMP_THRESHOLD && + !identityRecord.get('nonblockingApproval') + ); + }, + saveIdentityWithAttributes: function(identifier, attributes) { + if (identifier === null || identifier === undefined) { + throw new Error('Tried to put identity key for undefined/null key'); + } + var number = textsecure.utils.unencodeNumber(identifier)[0]; + return new Promise(function(resolve, reject) { + var identityRecord = new IdentityRecord({ id: number }); + identityRecord.set(attributes); + if (identityRecord.isValid()) { + // false if invalid attributes + identityRecord.save().then(resolve); + } else { + reject(identityRecord.validationError); + } + }); + }, + setApproval: function(identifier, nonblockingApproval) { + if (identifier === null || identifier === undefined) { + throw new Error('Tried to set approval for undefined/null identifier'); + } + if (typeof nonblockingApproval !== 'boolean') { + throw new Error('Invalid approval status'); + } + var number = textsecure.utils.unencodeNumber(identifier)[0]; + return new Promise(function(resolve, reject) { + var identityRecord = new IdentityRecord({ id: number }); + identityRecord.fetch().then(function() { + identityRecord + .save({ + nonblockingApproval: nonblockingApproval, + }) + .then( + function() { + resolve(); + }, + function() { + // catch + reject(new Error('No identity record for ' + number)); + } + ); + }); + }); + }, + setVerified: function(identifier, verifiedStatus, publicKey) { + if (identifier === null || identifier === undefined) { + throw new Error('Tried to set verified for undefined/null key'); + } + if (!validateVerifiedStatus(verifiedStatus)) { + throw new Error('Invalid verified status'); + } + if (arguments.length > 2 && !(publicKey instanceof ArrayBuffer)) { + throw new Error('Invalid public key'); + } + return new Promise(function(resolve, reject) { + var identityRecord = new IdentityRecord({ id: identifier }); + identityRecord.fetch().then( + function() { + if ( + !publicKey || + equalArrayBuffers(identityRecord.get('publicKey'), publicKey) + ) { + identityRecord.set({ verified: verifiedStatus }); + + if (identityRecord.isValid()) { + identityRecord.save({}).then(function() { + resolve(); + }, reject); + } else { + reject(identityRecord.validationError); + } + } else { + console.log('No identity record for specified publicKey'); + resolve(); + } + }, + function() { + // catch + reject(new Error('No identity record for ' + identifier)); + } + ); + }); + }, + getVerified: function(identifier) { + if (identifier === null || identifier === undefined) { + throw new Error('Tried to set verified for undefined/null key'); + } + return new Promise(function(resolve, reject) { + var identityRecord = new IdentityRecord({ id: identifier }); + identityRecord.fetch().then( + function() { + var verifiedStatus = identityRecord.get('verified'); + if (validateVerifiedStatus(verifiedStatus)) { + resolve(verifiedStatus); + } else { + resolve(VerifiedStatus.DEFAULT); + } + }, + function() { + // catch + reject(new Error('No identity record for ' + identifier)); + } + ); + }); + }, + // Resolves to true if a new identity key was saved + processContactSyncVerificationState: function( + identifier, + verifiedStatus, + publicKey + ) { + if (verifiedStatus === VerifiedStatus.UNVERIFIED) { + return this.processUnverifiedMessage( + identifier, + verifiedStatus, + publicKey + ); + } else { + return this.processVerifiedMessage( + identifier, + verifiedStatus, + publicKey + ); + } + }, + // This function encapsulates the non-Java behavior, since the mobile apps don't + // currently receive contact syncs and therefore will see a verify sync with + // UNVERIFIED status + processUnverifiedMessage: function(identifier, verifiedStatus, publicKey) { + if (identifier === null || identifier === undefined) { + throw new Error('Tried to set verified for undefined/null key'); + } + if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) { + throw new Error('Invalid public key'); + } + return new Promise( + function(resolve, reject) { + var identityRecord = new IdentityRecord({ id: identifier }); + var isPresent = false; + var isEqual = false; + identityRecord + .fetch() + .then(function() { + isPresent = true; + if (publicKey) { + isEqual = equalArrayBuffers( + publicKey, + identityRecord.get('publicKey') + ); + } + }) + .always( + function() { + if ( + isPresent && + isEqual && + identityRecord.get('verified') !== VerifiedStatus.UNVERIFIED + ) { + return textsecure.storage.protocol + .setVerified(identifier, verifiedStatus, publicKey) + .then(resolve, reject); + } + + if (!isPresent || !isEqual) { + return textsecure.storage.protocol + .saveIdentityWithAttributes(identifier, { + publicKey: publicKey, + verified: verifiedStatus, + firstUse: false, + timestamp: Date.now(), + nonblockingApproval: true, + }) + .then( + function() { + if (isPresent && !isEqual) { + this.trigger('keychange', identifier); + return this.archiveAllSessions(identifier).then( + function() { + // true signifies that we overwrote a previous key with a new one + return resolve(true); + }, + reject + ); + } + + return resolve(); + }.bind(this), + reject + ); + } + + // The situation which could get us here is: + // 1. had a previous key + // 2. new key is the same + // 3. desired new status is same as what we had before + return resolve(); + }.bind(this) + ); + }.bind(this) + ); + }, + // This matches the Java method as of + // https://github.com/signalapp/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188 + processVerifiedMessage: function(identifier, verifiedStatus, publicKey) { + if (identifier === null || identifier === undefined) { + throw new Error('Tried to set verified for undefined/null key'); + } + if (!validateVerifiedStatus(verifiedStatus)) { + throw new Error('Invalid verified status'); + } + if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) { + throw new Error('Invalid public key'); + } + return new Promise( + function(resolve, reject) { + var identityRecord = new IdentityRecord({ id: identifier }); + var isPresent = false; + var isEqual = false; + identityRecord + .fetch() + .then(function() { + isPresent = true; + if (publicKey) { + isEqual = equalArrayBuffers( + publicKey, + identityRecord.get('publicKey') + ); + } + }) + .always( + function() { + if (!isPresent && verifiedStatus === VerifiedStatus.DEFAULT) { + console.log('No existing record for default status'); + return resolve(); + } + + if ( + isPresent && + isEqual && + identityRecord.get('verified') !== VerifiedStatus.DEFAULT && + verifiedStatus === VerifiedStatus.DEFAULT + ) { + return textsecure.storage.protocol + .setVerified(identifier, verifiedStatus, publicKey) + .then(resolve, reject); + } + + if ( + verifiedStatus === VerifiedStatus.VERIFIED && + (!isPresent || + (isPresent && !isEqual) || + (isPresent && + identityRecord.get('verified') !== + VerifiedStatus.VERIFIED)) + ) { + return textsecure.storage.protocol + .saveIdentityWithAttributes(identifier, { + publicKey: publicKey, + verified: verifiedStatus, + firstUse: false, + timestamp: Date.now(), + nonblockingApproval: true, + }) + .then( + function() { + if (isPresent && !isEqual) { + this.trigger('keychange', identifier); + return this.archiveAllSessions(identifier).then( + function() { + // true signifies that we overwrote a previous key with a new one + return resolve(true); + }, + reject + ); + } + + return resolve(); + }.bind(this), + reject + ); + } + + // We get here if we got a new key and the status is DEFAULT. If the + // message is out of date, we don't want to lose whatever more-secure + // state we had before. + return resolve(); + }.bind(this) + ); + }.bind(this) + ); + }, + isUntrusted: function(identifier) { + if (identifier === null || identifier === undefined) { + throw new Error('Tried to set verified for undefined/null key'); + } + return new Promise(function(resolve, reject) { + var identityRecord = new IdentityRecord({ id: identifier }); + identityRecord.fetch().then( + function() { + if ( + Date.now() - identityRecord.get('timestamp') < + TIMESTAMP_THRESHOLD && + !identityRecord.get('nonblockingApproval') && + !identityRecord.get('firstUse') + ) { + resolve(true); + } else { + resolve(false); + } + }, + function() { + // catch + reject(new Error('No identity record for ' + identifier)); + } + ); + }); + }, + removeIdentityKey: function(number) { + return new Promise(function(resolve, reject) { + var identityRecord = new IdentityRecord({ id: number }); + identityRecord + .fetch() + .then(function() { + identityRecord.destroy(); + }) + .fail(function() { + reject(new Error('Tried to remove identity for unknown number')); + }); + resolve(textsecure.storage.protocol.removeAllSessions(number)); + }); + }, + + // Groups + getGroup: function(groupId) { + if (groupId === null || groupId === undefined) { + throw new Error('Tried to get group for undefined/null id'); + } + return new Promise(function(resolve) { + var group = new Group({ id: groupId }); + group.fetch().always(function() { + resolve(group.get('data')); + }); + }); + }, + putGroup: function(groupId, group) { + if (groupId === null || groupId === undefined) { + throw new Error('Tried to put group key for undefined/null id'); + } + if (group === null || group === undefined) { + throw new Error('Tried to put undefined/null group object'); + } + var group = new Group({ id: groupId, data: group }); + return new Promise(function(resolve) { + group.save().always(resolve); + }); + }, + removeGroup: function(groupId) { + if (groupId === null || groupId === undefined) { + throw new Error('Tried to remove group key for undefined/null id'); + } + return new Promise(function(resolve) { + var group = new Group({ id: groupId }); + group.destroy().always(resolve); + }); + }, + + // Not yet processed messages - for resiliency + getAllUnprocessed: function() { + var collection; + return new Promise(function(resolve, reject) { + collection = new UnprocessedCollection(); + return collection.fetch().then(resolve, reject); + }).then(function() { + // Return a plain array of plain objects + return collection.map('attributes'); + }); + }, + addUnprocessed: function(data) { + return new Promise(function(resolve, reject) { + var unprocessed = new Unprocessed(data); + return unprocessed.save().then(resolve, reject); + }); + }, + updateUnprocessed: function(id, updates) { + return new Promise( + function(resolve, reject) { + var unprocessed = new Unprocessed({ + id: id, + }); + return unprocessed.fetch().then(function() { + return unprocessed.save(updates).then(resolve, reject); + }, reject); + }.bind(this) + ); + }, + removeUnprocessed: function(id) { + return new Promise( + function(resolve, reject) { + var unprocessed = new Unprocessed({ + id: id, + }); + return unprocessed.destroy().then(resolve, reject); + }.bind(this) + ); + }, + removeAllData: function() { + // First the in-memory caches: + window.storage.reset(); // items store + ConversationController.reset(); // conversations store + + // Then, the entire database: + return Whisper.Database.clear(); + }, + removeAllConfiguration: function() { + // First the in-memory cache for the items store: + window.storage.reset(); + + // Then anything in the database that isn't a message/conversation/group: + return Whisper.Database.clearStores([ + 'items', + 'identityKeys', + 'sessions', + 'signedPreKeys', + 'preKeys', + 'unprocessed', + ]); + }, + }; + _.extend(SignalProtocolStore.prototype, Backbone.Events); + + window.SignalProtocolStore = SignalProtocolStore; + window.SignalProtocolStore.prototype.Direction = Direction; + window.SignalProtocolStore.prototype.VerifiedStatus = VerifiedStatus; })(); diff --git a/js/spell_check.js b/js/spell_check.js index cd22baae3..d27a2e57c 100644 --- a/js/spell_check.js +++ b/js/spell_check.js @@ -1,4 +1,4 @@ -(function () { +(function() { var electron = require('electron'); var remote = electron.remote; var app = remote.app; @@ -31,7 +31,7 @@ 'shouldn', 'wasn', 'weren', - 'wouldn' + 'wouldn', ]; function setupLinux(locale) { @@ -39,7 +39,12 @@ // apt-get install hunspell- can be run for easy access to other dictionaries var location = process.env.HUNSPELL_DICTIONARIES || '/usr/share/hunspell'; - console.log('Detected Linux. Setting up spell check with locale', locale, 'and dictionary location', location); + console.log( + 'Detected Linux. Setting up spell check with locale', + locale, + 'and dictionary location', + location + ); spellchecker.setDictionary(locale, location); } else { console.log('Detected Linux. Using default en_US spell check dictionary'); @@ -50,10 +55,17 @@ if (process.env.HUNSPELL_DICTIONARIES || locale !== 'en_US') { var location = process.env.HUNSPELL_DICTIONARIES; - console.log('Detected Windows 7 or below. Setting up spell-check with locale', locale, 'and dictionary location', location); + console.log( + 'Detected Windows 7 or below. Setting up spell-check with locale', + locale, + 'and dictionary location', + location + ); spellchecker.setDictionary(locale, location); } else { - console.log('Detected Windows 7 or below. Using default en_US spell check dictionary'); + console.log( + 'Detected Windows 7 or below. Using default en_US spell check dictionary' + ); } } @@ -69,14 +81,17 @@ if (process.platform === 'linux') { setupLinux(locale); - } else if (process.platform === 'windows' && semver.lt(os.release(), '8.0.0')) { + } else if ( + process.platform === 'windows' && + semver.lt(os.release(), '8.0.0') + ) { setupWin7AndEarlier(locale); } else { // OSX and Windows 8+ have OS-level spellcheck APIs console.log('Using OS-level spell check API with locale', process.env.LANG); } - var simpleChecker = window.spellChecker = { + var simpleChecker = (window.spellChecker = { spellCheck: function(text) { return !this.isMisspelled(text); }, @@ -101,8 +116,8 @@ }, add: function(text) { spellchecker.add(text); - } - }; + }, + }); webFrame.setSpellCheckProvider( 'en-US', @@ -120,7 +135,8 @@ var selectedText = window.getSelection().toString(); var isMisspelled = selectedText && simpleChecker.isMisspelled(selectedText); - var spellingSuggestions = isMisspelled && simpleChecker.getSuggestions(selectedText).slice(0, 5); + var spellingSuggestions = + isMisspelled && simpleChecker.getSuggestions(selectedText).slice(0, 5); var menu = buildEditorContextMenu({ isMisspelled: isMisspelled, spellingSuggestions: spellingSuggestions, diff --git a/js/storage.js b/js/storage.js index b130ee3ac..541addf95 100644 --- a/js/storage.js +++ b/js/storage.js @@ -1,80 +1,89 @@ /* * vim: ts=4:sw=4:expandtab */ -;(function() { - 'use strict'; - window.Whisper = window.Whisper || {}; - var Item = Backbone.Model.extend({ - database: Whisper.Database, - storeName: 'items' - }); - var ItemCollection = Backbone.Collection.extend({ - model: Item, - storeName: 'items', - database: Whisper.Database, - }); +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; + var Item = Backbone.Model.extend({ + database: Whisper.Database, + storeName: 'items', + }); + var ItemCollection = Backbone.Collection.extend({ + model: Item, + storeName: 'items', + database: Whisper.Database, + }); - var ready = false; - var items = new ItemCollection(); - items.on('reset', function() { ready = true; }); - window.storage = { - /***************************** - *** Base Storage Routines *** - *****************************/ - put: function(key, value) { - if (value === undefined) { - throw new Error("Tried to store undefined"); - } - if (!ready) { - console.log('Called storage.put before storage is ready. key:', key); - } - var item = items.add({id: key, value: value}, {merge: true}); - return new Promise(function(resolve, reject) { - item.save().then(resolve, reject); - }); - }, + var ready = false; + var items = new ItemCollection(); + items.on('reset', function() { + ready = true; + }); + window.storage = { + /***************************** + *** Base Storage Routines *** + *****************************/ + put: function(key, value) { + if (value === undefined) { + throw new Error('Tried to store undefined'); + } + if (!ready) { + console.log('Called storage.put before storage is ready. key:', key); + } + var item = items.add({ id: key, value: value }, { merge: true }); + return new Promise(function(resolve, reject) { + item.save().then(resolve, reject); + }); + }, - get: function(key, defaultValue) { - var item = items.get("" + key); - if (!item) { - return defaultValue; - } - return item.get('value'); - }, + get: function(key, defaultValue) { + var item = items.get('' + key); + if (!item) { + return defaultValue; + } + return item.get('value'); + }, - remove: function(key) { - var item = items.get("" + key); - if (item) { - items.remove(item); - return new Promise(function(resolve, reject) { - item.destroy().then(resolve, reject); - }); - } - return Promise.resolve(); - }, + remove: function(key) { + var item = items.get('' + key); + if (item) { + items.remove(item); + return new Promise(function(resolve, reject) { + item.destroy().then(resolve, reject); + }); + } + return Promise.resolve(); + }, - onready: function(callback) { - if (ready) { - callback(); - } else { - items.on('reset', callback); - } - }, + onready: function(callback) { + if (ready) { + callback(); + } else { + items.on('reset', callback); + } + }, - fetch: function() { - return new Promise((resolve, reject) => { - items.fetch({reset: true}) - .fail(() => reject(new Error('Failed to fetch from storage.' + - ' This may be due to an unexpected database version.'))) - .always(resolve); - }); - }, + fetch: function() { + return new Promise((resolve, reject) => { + items + .fetch({ reset: true }) + .fail(() => + reject( + new Error( + 'Failed to fetch from storage.' + + ' This may be due to an unexpected database version.' + ) + ) + ) + .always(resolve); + }); + }, - reset: function() { - items.reset(); - } - }; - window.textsecure = window.textsecure || {}; - window.textsecure.storage = window.textsecure.storage || {}; - window.textsecure.storage.impl = window.storage; + reset: function() { + items.reset(); + }, + }; + window.textsecure = window.textsecure || {}; + window.textsecure.storage = window.textsecure.storage || {}; + window.textsecure.storage.impl = window.storage; })(); diff --git a/js/views/app_view.js b/js/views/app_view.js index 662e7539e..5ace0bf31 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -1,168 +1,177 @@ -(function () { - 'use strict'; +(function() { + 'use strict'; - window.Whisper = window.Whisper || {}; + window.Whisper = window.Whisper || {}; - Whisper.AppView = Backbone.View.extend({ - initialize: function(options) { - this.inboxView = null; - this.installView = null; + Whisper.AppView = Backbone.View.extend({ + initialize: function(options) { + this.inboxView = null; + this.installView = null; - this.applyTheme(); - this.applyHideMenu(); - }, - events: { - 'click .openInstaller': 'openInstaller', // NetworkStatusView has this button - 'openInbox': 'openInbox', - 'change-theme': 'applyTheme', - 'change-hide-menu': 'applyHideMenu', - }, - applyTheme: function() { - var theme = storage.get('theme-setting') || 'android'; - this.$el.removeClass('ios') - .removeClass('android-dark') - .removeClass('android') - .addClass(theme); - }, - applyHideMenu: function() { - var hideMenuBar = storage.get('hide-menu-bar', false); - window.setAutoHideMenuBar(hideMenuBar); - window.setMenuBarVisibility(!hideMenuBar); - }, - openView: function(view) { - this.el.innerHTML = ""; - this.el.append(view.el); - this.delegateEvents(); - }, - openDebugLog: function() { - this.closeDebugLog(); - this.debugLogView = new Whisper.DebugLogView(); - this.debugLogView.$el.appendTo(this.el); - }, - closeDebugLog: function() { - if (this.debugLogView) { - this.debugLogView.remove(); - this.debugLogView = null; - } - }, - openImporter: function() { - window.addSetupMenuItems(); - this.resetViews(); - var importView = this.importView = new Whisper.ImportView(); - this.listenTo(importView, 'light-import', this.finishLightImport.bind(this)); - this.openView(this.importView); - }, - finishLightImport: function() { - var options = { - hasExistingData: true - }; - this.openInstaller(options); - }, - closeImporter: function() { - if (this.importView) { - this.importView.remove(); - this.importView = null; - } - }, - openInstaller: function(options) { - options = options || {}; + this.applyTheme(); + this.applyHideMenu(); + }, + events: { + 'click .openInstaller': 'openInstaller', // NetworkStatusView has this button + openInbox: 'openInbox', + 'change-theme': 'applyTheme', + 'change-hide-menu': 'applyHideMenu', + }, + applyTheme: function() { + var theme = storage.get('theme-setting') || 'android'; + this.$el + .removeClass('ios') + .removeClass('android-dark') + .removeClass('android') + .addClass(theme); + }, + applyHideMenu: function() { + var hideMenuBar = storage.get('hide-menu-bar', false); + window.setAutoHideMenuBar(hideMenuBar); + window.setMenuBarVisibility(!hideMenuBar); + }, + openView: function(view) { + this.el.innerHTML = ''; + this.el.append(view.el); + this.delegateEvents(); + }, + openDebugLog: function() { + this.closeDebugLog(); + this.debugLogView = new Whisper.DebugLogView(); + this.debugLogView.$el.appendTo(this.el); + }, + closeDebugLog: function() { + if (this.debugLogView) { + this.debugLogView.remove(); + this.debugLogView = null; + } + }, + openImporter: function() { + window.addSetupMenuItems(); + this.resetViews(); + var importView = (this.importView = new Whisper.ImportView()); + this.listenTo( + importView, + 'light-import', + this.finishLightImport.bind(this) + ); + this.openView(this.importView); + }, + finishLightImport: function() { + var options = { + hasExistingData: true, + }; + this.openInstaller(options); + }, + closeImporter: function() { + if (this.importView) { + this.importView.remove(); + this.importView = null; + } + }, + openInstaller: function(options) { + options = options || {}; - // If we're in the middle of import, we don't want to show the menu options - // allowing the user to switch to other ways to set up the app. If they - // switched back and forth in the middle of a light import, they'd lose all - // that imported data. - if (!options.hasExistingData) { - window.addSetupMenuItems(); - } + // If we're in the middle of import, we don't want to show the menu options + // allowing the user to switch to other ways to set up the app. If they + // switched back and forth in the middle of a light import, they'd lose all + // that imported data. + if (!options.hasExistingData) { + window.addSetupMenuItems(); + } - this.resetViews(); - var installView = this.installView = new Whisper.InstallView(options); - this.openView(this.installView); - }, - closeInstaller: function() { - if (this.installView) { - this.installView.remove(); - this.installView = null; - } - }, - openStandalone: function() { - if (window.config.environment !== 'production') { - window.addSetupMenuItems(); - this.resetViews(); - this.standaloneView = new Whisper.StandaloneRegistrationView(); - this.openView(this.standaloneView); - } - }, - closeStandalone: function() { - if (this.standaloneView) { - this.standaloneView.remove(); - this.standaloneView = null; - } - }, - resetViews: function() { - this.closeInstaller(); - this.closeImporter(); - this.closeStandalone(); - }, - openInbox: function(options) { - options = options || {}; - // The inbox can be created before the 'empty' event fires or afterwards. If - // before, it's straightforward: the onEmpty() handler below updates the - // view directly, and we're in good shape. If we create the inbox late, we - // need to be sure that the current value of initialLoadComplete is provided - // so its loading screen doesn't stick around forever. + this.resetViews(); + var installView = (this.installView = new Whisper.InstallView(options)); + this.openView(this.installView); + }, + closeInstaller: function() { + if (this.installView) { + this.installView.remove(); + this.installView = null; + } + }, + openStandalone: function() { + if (window.config.environment !== 'production') { + window.addSetupMenuItems(); + this.resetViews(); + this.standaloneView = new Whisper.StandaloneRegistrationView(); + this.openView(this.standaloneView); + } + }, + closeStandalone: function() { + if (this.standaloneView) { + this.standaloneView.remove(); + this.standaloneView = null; + } + }, + resetViews: function() { + this.closeInstaller(); + this.closeImporter(); + this.closeStandalone(); + }, + openInbox: function(options) { + options = options || {}; + // The inbox can be created before the 'empty' event fires or afterwards. If + // before, it's straightforward: the onEmpty() handler below updates the + // view directly, and we're in good shape. If we create the inbox late, we + // need to be sure that the current value of initialLoadComplete is provided + // so its loading screen doesn't stick around forever. - // Two primary techniques at play for this situation: - // - background.js has two openInbox() calls, and passes initalLoadComplete - // directly via the options parameter. - // - in other situations openInbox() will be called with no options. So this - // view keeps track of whether onEmpty() has ever been called with - // this.initialLoadComplete. An example of this: on a phone-pairing setup. - _.defaults(options, {initialLoadComplete: this.initialLoadComplete}); + // Two primary techniques at play for this situation: + // - background.js has two openInbox() calls, and passes initalLoadComplete + // directly via the options parameter. + // - in other situations openInbox() will be called with no options. So this + // view keeps track of whether onEmpty() has ever been called with + // this.initialLoadComplete. An example of this: on a phone-pairing setup. + _.defaults(options, { initialLoadComplete: this.initialLoadComplete }); - console.log('open inbox'); - this.closeInstaller(); + console.log('open inbox'); + this.closeInstaller(); - if (!this.inboxView) { - // We create the inbox immediately so we don't miss an update to - // this.initialLoadComplete between the start of this method and the - // creation of inboxView. - this.inboxView = new Whisper.InboxView({ - model: self, - window: window, - initialLoadComplete: options.initialLoadComplete - }); - return ConversationController.loadPromise().then(function() { - this.openView(this.inboxView); - }.bind(this)); - } else { - if (!$.contains(this.el, this.inboxView.el)) { - this.openView(this.inboxView); - } - window.focus(); // FIXME - return Promise.resolve(); - } - }, - onEmpty: function() { - var view = this.inboxView; + if (!this.inboxView) { + // We create the inbox immediately so we don't miss an update to + // this.initialLoadComplete between the start of this method and the + // creation of inboxView. + this.inboxView = new Whisper.InboxView({ + model: self, + window: window, + initialLoadComplete: options.initialLoadComplete, + }); + return ConversationController.loadPromise().then( + function() { + this.openView(this.inboxView); + }.bind(this) + ); + } else { + if (!$.contains(this.el, this.inboxView.el)) { + this.openView(this.inboxView); + } + window.focus(); // FIXME + return Promise.resolve(); + } + }, + onEmpty: function() { + var view = this.inboxView; - this.initialLoadComplete = true; - if (view) { - view.onEmpty(); - } - }, - onProgress: function(count) { - var view = this.inboxView; - if (view) { - view.onProgress(count); - } - }, - openConversation: function(conversation) { - if (conversation) { - this.openInbox().then(function() { - this.inboxView.openConversation(null, conversation); - }.bind(this)); - } - }, - }); + this.initialLoadComplete = true; + if (view) { + view.onEmpty(); + } + }, + onProgress: function(count) { + var view = this.inboxView; + if (view) { + view.onProgress(count); + } + }, + openConversation: function(conversation) { + if (conversation) { + this.openInbox().then( + function() { + this.inboxView.openConversation(null, conversation); + }.bind(this) + ); + } + }, + }); })(); diff --git a/js/views/attachment_preview_view.js b/js/views/attachment_preview_view.js index 69f931b59..7e33b0ecc 100644 --- a/js/views/attachment_preview_view.js +++ b/js/views/attachment_preview_view.js @@ -1,15 +1,15 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.AttachmentPreviewView = Whisper.View.extend({ - className: 'attachment-preview', - templateName: 'attachment-preview', - render_attributes: function() { - return {source: this.src}; - } - }); + Whisper.AttachmentPreviewView = Whisper.View.extend({ + className: 'attachment-preview', + templateName: 'attachment-preview', + render_attributes: function() { + return { source: this.src }; + }, + }); })(); diff --git a/js/views/attachment_view.js b/js/views/attachment_view.js index e3e6dbfd4..921df4df0 100644 --- a/js/views/attachment_view.js +++ b/js/views/attachment_view.js @@ -9,7 +9,7 @@ /* global Whisper: false */ // eslint-disable-next-line func-names -(function () { +(function() { 'use strict'; const FileView = Whisper.View.extend({ @@ -62,10 +62,7 @@ const VideoView = MediaView.extend({ tagName: 'video' }); // Blacklist common file types known to be unsupported in Chrome - const unsupportedFileTypes = [ - 'audio/aiff', - 'video/quicktime', - ]; + const unsupportedFileTypes = ['audio/aiff', 'video/quicktime']; Whisper.AttachmentView = Backbone.View.extend({ tagName: 'div', @@ -123,7 +120,10 @@ }, isVoiceMessage() { // eslint-disable-next-line no-bitwise - if (this.model.flags & textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE) { + if ( + this.model.flags & + textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE + ) { return true; } @@ -241,4 +241,4 @@ this.trigger('update'); }, }); -}()); +})(); diff --git a/js/views/banner_view.js b/js/views/banner_view.js index f39613262..7ad2cc757 100644 --- a/js/views/banner_view.js +++ b/js/views/banner_view.js @@ -1,36 +1,36 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.BannerView = Whisper.View.extend({ - className: 'banner', - templateName: 'banner', - events: { - 'click .dismiss': 'onDismiss', - 'click .body': 'onClick', - }, - initialize: function(options) { - this.message = options.message; - this.callbacks = { - onDismiss: options.onDismiss, - onClick: options.onClick - }; - this.render(); - }, - render_attributes: function() { - return { - message: this.message - }; - }, - onDismiss: function(e) { - this.callbacks.onDismiss(); - e.stopPropagation(); - }, - onClick: function() { - this.callbacks.onClick(); - } - }); + Whisper.BannerView = Whisper.View.extend({ + className: 'banner', + templateName: 'banner', + events: { + 'click .dismiss': 'onDismiss', + 'click .body': 'onClick', + }, + initialize: function(options) { + this.message = options.message; + this.callbacks = { + onDismiss: options.onDismiss, + onClick: options.onClick, + }; + this.render(); + }, + render_attributes: function() { + return { + message: this.message, + }; + }, + onDismiss: function(e) { + this.callbacks.onDismiss(); + e.stopPropagation(); + }, + onClick: function() { + this.callbacks.onClick(); + }, + }); })(); diff --git a/js/views/confirmation_dialog_view.js b/js/views/confirmation_dialog_view.js index f22ba48e0..8937a4da1 100644 --- a/js/views/confirmation_dialog_view.js +++ b/js/views/confirmation_dialog_view.js @@ -1,57 +1,57 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.ConfirmationDialogView = Whisper.View.extend({ - className: 'confirmation-dialog modal', - templateName: 'confirmation-dialog', - initialize: function(options) { - this.message = options.message; - this.hideCancel = options.hideCancel; + Whisper.ConfirmationDialogView = Whisper.View.extend({ + className: 'confirmation-dialog modal', + templateName: 'confirmation-dialog', + initialize: function(options) { + this.message = options.message; + this.hideCancel = options.hideCancel; - this.resolve = options.resolve; - this.okText = options.okText || i18n('ok'); + this.resolve = options.resolve; + this.okText = options.okText || i18n('ok'); - this.reject = options.reject; - this.cancelText = options.cancelText || i18n('cancel'); + this.reject = options.reject; + this.cancelText = options.cancelText || i18n('cancel'); - this.render(); - }, - events: { - 'keyup': 'onKeyup', - 'click .ok': 'ok', - 'click .cancel': 'cancel', - }, - render_attributes: function() { - return { - message: this.message, - showCancel: !this.hideCancel, - cancel: this.cancelText, - ok: this.okText - }; - }, - ok: function() { - this.remove(); - if (this.resolve) { - this.resolve(); - } - }, - cancel: function() { - this.remove(); - if (this.reject) { - this.reject(); - } - }, - onKeyup: function(event) { - if (event.key === 'Escape' || event.key === 'Esc') { - this.cancel(); - } - }, - focusCancel: function() { - this.$('.cancel').focus(); - } - }); + this.render(); + }, + events: { + keyup: 'onKeyup', + 'click .ok': 'ok', + 'click .cancel': 'cancel', + }, + render_attributes: function() { + return { + message: this.message, + showCancel: !this.hideCancel, + cancel: this.cancelText, + ok: this.okText, + }; + }, + ok: function() { + this.remove(); + if (this.resolve) { + this.resolve(); + } + }, + cancel: function() { + this.remove(); + if (this.reject) { + this.reject(); + } + }, + onKeyup: function(event) { + if (event.key === 'Escape' || event.key === 'Esc') { + this.cancel(); + } + }, + focusCancel: function() { + this.$('.cancel').focus(); + }, + }); })(); diff --git a/js/views/contact_list_view.js b/js/views/contact_list_view.js index 4aacbdbd7..3b3f619b1 100644 --- a/js/views/contact_list_view.js +++ b/js/views/contact_list_view.js @@ -1,53 +1,53 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.ContactListView = Whisper.ListView.extend({ - tagName: 'div', - itemView: Whisper.View.extend({ - tagName: 'div', - className: 'contact', - templateName: 'contact', - events: { - 'click': 'showIdentity' - }, - initialize: function(options) { - this.ourNumber = textsecure.storage.user.getNumber(); - this.listenBack = options.listenBack; + Whisper.ContactListView = Whisper.ListView.extend({ + tagName: 'div', + itemView: Whisper.View.extend({ + tagName: 'div', + className: 'contact', + templateName: 'contact', + events: { + click: 'showIdentity', + }, + initialize: function(options) { + this.ourNumber = textsecure.storage.user.getNumber(); + this.listenBack = options.listenBack; - this.listenTo(this.model, 'change', this.render); - }, - render_attributes: function() { - if (this.model.id === this.ourNumber) { - return { - title: i18n('me'), - number: this.model.getNumber(), - avatar: this.model.getAvatar() - }; - } + this.listenTo(this.model, 'change', this.render); + }, + render_attributes: function() { + if (this.model.id === this.ourNumber) { + return { + title: i18n('me'), + number: this.model.getNumber(), + avatar: this.model.getAvatar(), + }; + } - return { - class: 'clickable', - title: this.model.getTitle(), - number: this.model.getNumber(), - avatar: this.model.getAvatar(), - profileName: this.model.getProfileName(), - isVerified: this.model.isVerified(), - verified: i18n('verified') - }; - }, - showIdentity: function() { - if (this.model.id === this.ourNumber) { - return; - } - var view = new Whisper.KeyVerificationPanelView({ - model: this.model - }); - this.listenBack(view); - } - }) - }); + return { + class: 'clickable', + title: this.model.getTitle(), + number: this.model.getNumber(), + avatar: this.model.getAvatar(), + profileName: this.model.getProfileName(), + isVerified: this.model.isVerified(), + verified: i18n('verified'), + }; + }, + showIdentity: function() { + if (this.model.id === this.ourNumber) { + return; + } + var view = new Whisper.KeyVerificationPanelView({ + model: this.model, + }); + this.listenBack(view); + }, + }), + }); })(); diff --git a/js/views/conversation_list_item_view.js b/js/views/conversation_list_item_view.js index 7b7d92b97..1d9689525 100644 --- a/js/views/conversation_list_item_view.js +++ b/js/views/conversation_list_item_view.js @@ -1,73 +1,92 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - // list of conversations, showing user/group and last message sent - Whisper.ConversationListItemView = Whisper.View.extend({ - tagName: 'div', - className: function() { - return 'conversation-list-item contact ' + this.model.cid; - }, - templateName: 'conversation-preview', - events: { - 'click': 'select' - }, - initialize: function() { - // auto update - this.listenTo(this.model, 'change', _.debounce(this.render.bind(this), 1000)); - this.listenTo(this.model, 'destroy', this.remove); // auto update - this.listenTo(this.model, 'opened', this.markSelected); // auto update + // list of conversations, showing user/group and last message sent + Whisper.ConversationListItemView = Whisper.View.extend({ + tagName: 'div', + className: function() { + return 'conversation-list-item contact ' + this.model.cid; + }, + templateName: 'conversation-preview', + events: { + click: 'select', + }, + initialize: function() { + // auto update + this.listenTo( + this.model, + 'change', + _.debounce(this.render.bind(this), 1000) + ); + this.listenTo(this.model, 'destroy', this.remove); // auto update + this.listenTo(this.model, 'opened', this.markSelected); // auto update - var updateLastMessage = _.debounce(this.model.updateLastMessage.bind(this.model), 1000); - this.listenTo(this.model.messageCollection, 'add remove', updateLastMessage); - this.listenTo(this.model, 'newmessage', updateLastMessage); + var updateLastMessage = _.debounce( + this.model.updateLastMessage.bind(this.model), + 1000 + ); + this.listenTo( + this.model.messageCollection, + 'add remove', + updateLastMessage + ); + this.listenTo(this.model, 'newmessage', updateLastMessage); - extension.windows.onClosed(function() { - this.stopListening(); - }.bind(this)); - this.timeStampView = new Whisper.TimestampView({brief: true}); - this.model.updateLastMessage(); - }, + extension.windows.onClosed( + function() { + this.stopListening(); + }.bind(this) + ); + this.timeStampView = new Whisper.TimestampView({ brief: true }); + this.model.updateLastMessage(); + }, - markSelected: function() { - this.$el.addClass('selected').siblings('.selected').removeClass('selected'); - }, + markSelected: function() { + this.$el + .addClass('selected') + .siblings('.selected') + .removeClass('selected'); + }, - select: function(e) { - this.markSelected(); - this.$el.trigger('select', this.model); - }, + select: function(e) { + this.markSelected(); + this.$el.trigger('select', this.model); + }, - render: function() { - this.$el.html( - Mustache.render(_.result(this,'template', ''), { - title: this.model.getTitle(), - last_message: this.model.get('lastMessage'), - last_message_timestamp: this.model.get('timestamp'), - number: this.model.getNumber(), - avatar: this.model.getAvatar(), - profileName: this.model.getProfileName(), - unreadCount: this.model.get('unreadCount') - }, this.render_partials()) - ); - this.timeStampView.setElement(this.$('.last-timestamp')); - this.timeStampView.update(); + render: function() { + this.$el.html( + Mustache.render( + _.result(this, 'template', ''), + { + title: this.model.getTitle(), + last_message: this.model.get('lastMessage'), + last_message_timestamp: this.model.get('timestamp'), + number: this.model.getNumber(), + avatar: this.model.getAvatar(), + profileName: this.model.getProfileName(), + unreadCount: this.model.get('unreadCount'), + }, + this.render_partials() + ) + ); + this.timeStampView.setElement(this.$('.last-timestamp')); + this.timeStampView.update(); - emoji_util.parse(this.$('.name')); - emoji_util.parse(this.$('.last-message')); + emoji_util.parse(this.$('.name')); + emoji_util.parse(this.$('.last-message')); - var unread = this.model.get('unreadCount'); - if (unread > 0) { - this.$el.addClass('unread'); - } else { - this.$el.removeClass('unread'); - } + var unread = this.model.get('unreadCount'); + if (unread > 0) { + this.$el.addClass('unread'); + } else { + this.$el.removeClass('unread'); + } - return this; - } - - }); + return this; + }, + }); })(); diff --git a/js/views/conversation_list_view.js b/js/views/conversation_list_view.js index 52fcd3ac1..b7608ce86 100644 --- a/js/views/conversation_list_view.js +++ b/js/views/conversation_list_view.js @@ -1,61 +1,61 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.ConversationListView = Whisper.ListView.extend({ - tagName: 'div', - itemView: Whisper.ConversationListItemView, - updateLocation: function(conversation) { - var $el = this.$('.' + conversation.cid); + Whisper.ConversationListView = Whisper.ListView.extend({ + tagName: 'div', + itemView: Whisper.ConversationListItemView, + updateLocation: function(conversation) { + var $el = this.$('.' + conversation.cid); - if (!$el || !$el.length) { - console.log( - 'updateLocation: did not find element for conversation', - conversation.idForLogging() - ); - return; - } - if ($el.length > 1) { - console.log( - 'updateLocation: found more than one element for conversation', - conversation.idForLogging() - ); - return; - } + if (!$el || !$el.length) { + console.log( + 'updateLocation: did not find element for conversation', + conversation.idForLogging() + ); + return; + } + if ($el.length > 1) { + console.log( + 'updateLocation: found more than one element for conversation', + conversation.idForLogging() + ); + return; + } - var $allConversations = this.$('.conversation-list-item'); - var inboxCollection = getInboxCollection(); - var index = inboxCollection.indexOf(conversation); + var $allConversations = this.$('.conversation-list-item'); + var inboxCollection = getInboxCollection(); + var index = inboxCollection.indexOf(conversation); - var elIndex = $allConversations.index($el); - if (elIndex < 0) { - console.log( - 'updateLocation: did not find index for conversation', - conversation.idForLogging() - ); - } + var elIndex = $allConversations.index($el); + if (elIndex < 0) { + console.log( + 'updateLocation: did not find index for conversation', + conversation.idForLogging() + ); + } - if (index === elIndex) { - return; - } - if (index === 0) { - this.$el.prepend($el); - } else if (index === this.collection.length - 1) { - this.$el.append($el); - } else { - var targetConversation = inboxCollection.at(index - 1); - var target = this.$('.' + targetConversation.cid); - $el.insertAfter(target); - } - }, - removeItem: function(conversation) { - var $el = this.$('.' + conversation.cid); - if ($el && $el.length > 0) { - $el.remove(); - } - } - }); + if (index === elIndex) { + return; + } + if (index === 0) { + this.$el.prepend($el); + } else if (index === this.collection.length - 1) { + this.$el.append($el); + } else { + var targetConversation = inboxCollection.at(index - 1); + var target = this.$('.' + targetConversation.cid); + $el.insertAfter(target); + } + }, + removeItem: function(conversation) { + var $el = this.$('.' + conversation.cid); + if ($el && $el.length > 0) { + $el.remove(); + } + }, + }); })(); diff --git a/js/views/conversation_search_view.js b/js/views/conversation_search_view.js index 7a0cbb114..a34d1760f 100644 --- a/js/views/conversation_search_view.js +++ b/js/views/conversation_search_view.js @@ -3,13 +3,12 @@ /* global Whisper: false */ // eslint-disable-next-line func-names -(function () { +(function() { 'use strict'; window.Whisper = window.Whisper || {}; - const isSearchable = conversation => - conversation.isSearchable(); + const isSearchable = conversation => conversation.isSearchable(); Whisper.NewContactView = Whisper.View.extend({ templateName: 'new-contact', @@ -46,7 +45,9 @@ // View to display the matched contacts from typeahead this.typeahead_view = new Whisper.ConversationListView({ collection: new Whisper.ConversationCollection([], { - comparator(m) { return m.getTitle().toLowerCase(); }, + comparator(m) { + return m.getTitle().toLowerCase(); + }, }), }); this.$el.append(this.typeahead_view.el); @@ -75,8 +76,11 @@ /* eslint-disable more/no-then */ this.pending = this.pending.then(() => this.typeahead.search(query).then(() => { - this.typeahead_view.collection.reset(this.typeahead.filter(isSearchable)); - })); + this.typeahead_view.collection.reset( + this.typeahead.filter(isSearchable) + ); + }) + ); /* eslint-enable more/no-then */ this.trigger('show'); } else { @@ -105,8 +109,10 @@ } const newConversationId = this.new_contact_view.model.id; - const conversation = - await ConversationController.getOrCreateAndWait(newConversationId, 'private'); + const conversation = await ConversationController.getOrCreateAndWait( + newConversationId, + 'private' + ); this.trigger('open', conversation); this.initNewContact(); this.resetTypeahead(); @@ -129,7 +135,9 @@ // eslint-disable-next-line more/no-then this.typeahead.fetchAlphabetical().then(() => { if (this.typeahead.length > 0) { - this.typeahead_view.collection.reset(this.typeahead.filter(isSearchable)); + this.typeahead_view.collection.reset( + this.typeahead.filter(isSearchable) + ); } else { this.showHints(); } @@ -163,4 +171,4 @@ return number.replace(/[\s-.()]*/g, '').match(/^\+?[0-9]*$/); }, }); -}()); +})(); diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 44bbf8747..a78e01b0f 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -13,7 +13,7 @@ /* global Whisper: false */ // eslint-disable-next-line func-names -(function () { +(function() { 'use strict'; window.Whisper = window.Whisper || {}; @@ -120,20 +120,32 @@ this.listenTo(this.model, 'destroy', this.stopListening); this.listenTo(this.model, 'change:verified', this.onVerifiedChange); this.listenTo(this.model, 'change:color', this.updateColor); - this.listenTo(this.model, 'change:avatar change:profileAvatar', this.updateAvatar); + this.listenTo( + this.model, + 'change:avatar change:profileAvatar', + this.updateAvatar + ); this.listenTo(this.model, 'newmessage', this.addMessage); this.listenTo(this.model, 'delivered', this.updateMessage); this.listenTo(this.model, 'read', this.updateMessage); this.listenTo(this.model, 'opened', this.onOpened); this.listenTo(this.model, 'expired', this.onExpired); this.listenTo(this.model, 'prune', this.onPrune); - this.listenTo(this.model.messageCollection, 'expired', this.onExpiredCollection); + this.listenTo( + this.model.messageCollection, + 'expired', + this.onExpiredCollection + ); this.listenTo( this.model.messageCollection, 'scroll-to-message', this.scrollToMessage ); - this.listenTo(this.model.messageCollection, 'reply', this.setQuoteMessage); + this.listenTo( + this.model.messageCollection, + 'reply', + this.setQuoteMessage + ); this.lazyUpdateVerified = _.debounce( this.model.updateVerified.bind(this.model), @@ -247,7 +259,7 @@ return; } - const oneHourAgo = Date.now() - (60 * 60 * 1000); + const oneHourAgo = Date.now() - 60 * 60 * 1000; if (this.isHidden() && this.lastActivity < oneHourAgo) { this.unload('inactivity'); } else if (this.view.atBottom()) { @@ -301,7 +313,7 @@ this.remove(); - this.model.messageCollection.forEach((model) => { + this.model.messageCollection.forEach(model => { model.trigger('unload'); }); this.model.messageCollection.reset([]); @@ -333,19 +345,21 @@ ); this.model.messageCollection.remove(models); - _.forEach(models, (model) => { + _.forEach(models, model => { model.trigger('unload'); }); }, markAllAsVerifiedDefault(unverified) { - return Promise.all(unverified.map((contact) => { - if (contact.isUnverified()) { - return contact.setVerifiedDefault(); - } + return Promise.all( + unverified.map(contact => { + if (contact.isUnverified()) { + return contact.setVerifiedDefault(); + } - return null; - })); + return null; + }) + ); }, markAllAsApproved(untrusted) { @@ -404,7 +418,10 @@ } }, toggleMicrophone() { - if (this.$('.send-message').val().length > 0 || this.fileInput.hasFiles()) { + if ( + this.$('.send-message').val().length > 0 || + this.fileInput.hasFiles() + ) { this.$('.capture-audio').hide(); } else { this.$('.capture-audio').show(); @@ -495,11 +512,13 @@ const statusPromise = this.throttledGetProfiles(); // eslint-disable-next-line more/no-then - this.statusFetch = statusPromise.then(() => this.model.updateVerified().then(() => { - this.onVerifiedChange(); - this.statusFetch = null; - console.log('done with status fetch'); - })); + this.statusFetch = statusPromise.then(() => + this.model.updateVerified().then(() => { + this.onVerifiedChange(); + this.statusFetch = null; + console.log('done with status fetch'); + }) + ); // We schedule our catch-up decrypt right after any in-progress fetch of // messages from the database, then ensure that the loading screen is only @@ -587,20 +606,25 @@ const conversationId = this.model.get('id'); const WhisperMessageCollection = Whisper.MessageCollection; - const rawMedia = await Signal.Backbone.Conversation.fetchVisualMediaAttachments({ - conversationId, - count: DEFAULT_MEDIA_FETCH_COUNT, - WhisperMessageCollection, - }); - const documents = await Signal.Backbone.Conversation.fetchFileAttachments({ - conversationId, - count: DEFAULT_DOCUMENTS_FETCH_COUNT, - WhisperMessageCollection, - }); + const rawMedia = await Signal.Backbone.Conversation.fetchVisualMediaAttachments( + { + conversationId, + count: DEFAULT_MEDIA_FETCH_COUNT, + WhisperMessageCollection, + } + ); + const documents = await Signal.Backbone.Conversation.fetchFileAttachments( + { + conversationId, + count: DEFAULT_DOCUMENTS_FETCH_COUNT, + WhisperMessageCollection, + } + ); // NOTE: Could we show grid previews from disk as well? - const loadMessages = Signal.Components.Types.Message - .loadWithObjectURL(Signal.Migrations.loadMessage); + const loadMessages = Signal.Components.Types.Message.loadWithObjectURL( + Signal.Migrations.loadMessage + ); const media = await loadMessages(rawMedia); const { getAbsoluteAttachmentPath } = Signal.Migrations; @@ -624,13 +648,15 @@ case 'media': { const mediaWithObjectURL = media.map(mediaMessage => - Object.assign( - {}, - mediaMessage, - { objectURL: getAbsoluteAttachmentPath(mediaMessage.attachments[0].path) } - )); - const selectedIndex = media.findIndex(mediaMessage => - mediaMessage.id === message.id); + Object.assign({}, mediaMessage, { + objectURL: getAbsoluteAttachmentPath( + mediaMessage.attachments[0].path + ), + }) + ); + const selectedIndex = media.findIndex( + mediaMessage => mediaMessage.id === message.id + ); this.lightboxGalleryView = new Whisper.ReactWrapperView({ Component: Signal.Components.LightboxGallery, props: { @@ -684,7 +710,7 @@ // We need to iterate here because unseen non-messages do not contribute to // the badge number, but should be reflected in the indicator's count. - this.model.messageCollection.forEach((model) => { + this.model.messageCollection.forEach(model => { if (!model.get('unread')) { return; } @@ -744,7 +770,7 @@ const delta = endingHeight - startingHeight; const height = this.view.outerHeight; - const newScrollPosition = (this.view.scrollPosition + delta) - height; + const newScrollPosition = this.view.scrollPosition + delta - height; this.view.$el.scrollTop(newScrollPosition); }, 1); }, @@ -759,15 +785,17 @@ // Avoiding await, since we want to capture the promise and make it available via // this.inProgressFetch // eslint-disable-next-line more/no-then - this.inProgressFetch = this.model.fetchContacts() + this.inProgressFetch = this.model + .fetchContacts() .then(() => this.model.fetchMessages()) .then(() => { this.$('.bar-container').hide(); - this.model.messageCollection.where({ unread: 1 }).forEach((m) => { + this.model.messageCollection.where({ unread: 1 }).forEach(m => { m.fetch(); }); this.inProgressFetch = null; - }).catch((error) => { + }) + .catch(error => { console.log( 'fetchMessages error:', error && error.stack ? error.stack : error @@ -820,8 +848,10 @@ // The conversation is visible, but window is not focused if (!this.lastSeenIndicator) { this.resetLastSeenIndicator({ scroll: false }); - } else if (this.view.atBottom() && - this.model.get('unreadCount') === this.lastSeenIndicator.getCount()) { + } else if ( + this.view.atBottom() && + this.model.get('unreadCount') === this.lastSeenIndicator.getCount() + ) { // The count check ensures that the last seen indicator is still in // sync with the real number of unread, so we can scroll to it. // We only do this if we're at the bottom, because that signals that @@ -1215,9 +1245,8 @@ }), }); - const selector = storage.get('theme-setting') === 'ios' - ? '.bottom-bar' - : '.send'; + const selector = + storage.get('theme-setting') === 'ios' ? '.bottom-bar' : '.send'; this.$(selector).prepend(this.quoteView.el); this.updateMessageFieldSize({}); @@ -1275,7 +1304,7 @@ }, replace_colons(str) { - return str.replace(emoji.rx_colons, (m) => { + return str.replace(emoji.rx_colons, m => { const idx = m.substr(1, m.length - 2); const val = emoji.map.colons[idx]; if (val) { @@ -1310,7 +1339,12 @@ updateMessageFieldSize(event) { const keyCode = event.which || event.keyCode; - if (keyCode === 13 && !event.altKey && !event.shiftKey && !event.ctrlKey) { + if ( + keyCode === 13 && + !event.altKey && + !event.shiftKey && + !event.ctrlKey + ) { // enter pressed - submit the form now event.preventDefault(); this.$('.bottom-bar form').submit(); @@ -1329,7 +1363,8 @@ ? this.quoteView.$el.outerHeight(includeMargin) : 0; - const height = this.$messageField.outerHeight() + + const height = + this.$messageField.outerHeight() + $attachmentPreviews.outerHeight() + this.$emojiPanelContainer.outerHeight() + quoteHeight + @@ -1350,8 +1385,10 @@ }, isHidden() { - return this.$el.css('display') === 'none' || - this.$('.panel').css('display') === 'none'; + return ( + this.$el.css('display') === 'none' || + this.$('.panel').css('display') === 'none' + ); }, }); -}()); +})(); diff --git a/js/views/debug_log_view.js b/js/views/debug_log_view.js index 6e8027cf6..8230d5ad8 100644 --- a/js/views/debug_log_view.js +++ b/js/views/debug_log_view.js @@ -2,7 +2,7 @@ /* global Whisper: false */ // eslint-disable-next-line func-names -(function () { +(function() { 'use strict'; window.Whisper = window.Whisper || {}; @@ -27,7 +27,7 @@ this.$('textarea').val(i18n('loading')); // eslint-disable-next-line more/no-then - window.log.fetch().then((text) => { + window.log.fetch().then(text => { this.$('textarea').val(text); }); }, @@ -63,7 +63,9 @@ }); this.$('.loading').removeClass('loading'); view.render(); - this.$('.link').focus().select(); + this.$('.link') + .focus() + .select(); }, }); -}()); +})(); diff --git a/js/views/error_view.js b/js/views/error_view.js index 3c9558f92..8d7ee6aea 100644 --- a/js/views/error_view.js +++ b/js/views/error_view.js @@ -1,16 +1,16 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; +(function() { + 'use strict'; - window.Whisper = window.Whisper || {}; + window.Whisper = window.Whisper || {}; - var ErrorView = Whisper.View.extend({ - className: 'error', - templateName: 'generic-error', - render_attributes: function() { - return this.model; - } - }); + var ErrorView = Whisper.View.extend({ + className: 'error', + templateName: 'generic-error', + render_attributes: function() { + return this.model; + }, + }); })(); diff --git a/js/views/file_input_view.js b/js/views/file_input_view.js index fd8cb1b3f..3f4b9298a 100644 --- a/js/views/file_input_view.js +++ b/js/views/file_input_view.js @@ -7,7 +7,7 @@ /* global Signal: false */ // eslint-disable-next-line func-names -(function () { +(function() { 'use strict'; window.Whisper = window.Whisper || {}; @@ -29,7 +29,7 @@ }); function makeImageThumbnail(size, objectUrl) { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { const img = document.createElement('img'); img.onerror = reject; img.onload = () => { @@ -60,18 +60,20 @@ resolve(blob); }; img.src = objectUrl; - })); + }); } function makeVideoScreenshot(objectUrl) { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { const video = document.createElement('video'); function capture() { const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; - canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height); + canvas + .getContext('2d') + .drawImage(video, 0, 0, canvas.width, canvas.height); const image = window.dataURLToBlobSync(canvas.toDataURL('image/png')); @@ -81,7 +83,7 @@ } video.addEventListener('canplay', capture); - video.addEventListener('error', (error) => { + video.addEventListener('error', error => { console.log( 'makeVideoThumbnail error', Signal.Types.Errors.toLogFormat(error) @@ -90,7 +92,7 @@ }); video.src = objectUrl; - })); + }); } function blobToArrayBuffer(blob) { @@ -123,7 +125,7 @@ className: 'file-input', initialize(options) { this.$input = this.$('input[type=file]'); - this.$input.click((e) => { + this.$input.click(e => { e.stopPropagation(); }); this.thumb = new Whisper.AttachmentPreviewView(); @@ -146,15 +148,18 @@ e.preventDefault(); // hack if (this.window && this.window.chrome && this.window.chrome.fileSystem) { - this.window.chrome.fileSystem.chooseEntry({ type: 'openFile' }, (entry) => { - if (!entry) { - return; + this.window.chrome.fileSystem.chooseEntry( + { type: 'openFile' }, + entry => { + if (!entry) { + return; + } + entry.file(file => { + this.file = file; + this.previewImages(); + }); } - entry.file((file) => { - this.file = file; - this.previewImages(); - }); - }); + ); } else { this.$input.click(); } @@ -178,14 +183,16 @@ }, autoScale(file) { - if (file.type.split('/')[0] !== 'image' || - file.type === 'image/gif' || - file.type === 'image/tiff') { + if ( + file.type.split('/')[0] !== 'image' || + file.type === 'image/gif' || + file.type === 'image/tiff' + ) { // nothing to do return Promise.resolve(file); } - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { const url = URL.createObjectURL(file); const img = document.createElement('img'); img.onerror = reject; @@ -195,13 +202,19 @@ const maxSize = 6000 * 1024; const maxHeight = 4096; const maxWidth = 4096; - if (img.width <= maxWidth && img.height <= maxHeight && file.size <= maxSize) { + if ( + img.width <= maxWidth && + img.height <= maxHeight && + file.size <= maxSize + ) { resolve(file); return; } const canvas = loadImage.scale(img, { - canvas: true, maxWidth, maxHeight, + canvas: true, + maxWidth, + maxHeight, }); let quality = 0.95; @@ -209,8 +222,10 @@ let blob; do { i -= 1; - blob = window.dataURLToBlobSync(canvas.toDataURL('image/jpeg', quality)); - quality = (quality * maxSize) / blob.size; + blob = window.dataURLToBlobSync( + canvas.toDataURL('image/jpeg', quality) + ); + quality = quality * maxSize / blob.size; // NOTE: During testing with a large image, we observed the // `quality` value being > 1. Should we clamp it to [0.5, 1.0]? // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax @@ -222,7 +237,7 @@ resolve(blob); }; img.src = url; - })); + }); }, async previewImages() { @@ -271,21 +286,25 @@ const blob = await this.autoScale(file); let limitKb = 1000000; - const blobType = file.type === 'image/gif' - ? 'gif' - : contentType.split('/')[0]; + const blobType = + file.type === 'image/gif' ? 'gif' : contentType.split('/')[0]; switch (blobType) { case 'image': - limitKb = 6000; break; + limitKb = 6000; + break; case 'gif': - limitKb = 25000; break; + limitKb = 25000; + break; case 'audio': - limitKb = 100000; break; + limitKb = 100000; + break; case 'video': - limitKb = 100000; break; + limitKb = 100000; + break; default: - limitKb = 100000; break; + limitKb = 100000; + break; } if ((blob.size / 1024).toFixed(4) >= limitKb) { const units = ['kB', 'MB', 'GB']; @@ -310,7 +329,9 @@ }, getFiles() { - const files = this.file ? [this.file] : Array.from(this.$input.prop('files')); + const files = this.file + ? [this.file] + : Array.from(this.$input.prop('files')); const promise = Promise.all(files.map(file => this.getFile(file))); this.clearForm(); return promise; @@ -325,7 +346,7 @@ ? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE : null; - const setFlags = flags => (attachment) => { + const setFlags = flags => attachment => { const newAttachment = Object.assign({}, attachment); if (flags) { newAttachment.flags = flags; @@ -345,9 +366,11 @@ // Scale and crop an image to 256px square const size = 256; const file = this.file || this.$input.prop('files')[0]; - if (file === undefined || + if ( + file === undefined || file.type.split('/')[0] !== 'image' || - file.type === 'image/gif') { + file.type === 'image/gif' + ) { // nothing to do return Promise.resolve(); } @@ -362,9 +385,9 @@ // File -> Promise Attachment readFile(file) { - return new Promise(((resolve, reject) => { + return new Promise((resolve, reject) => { const FR = new FileReader(); - FR.onload = (e) => { + FR.onload = e => { resolve({ data: e.target.result, contentType: file.type, @@ -375,7 +398,7 @@ FR.onerror = reject; FR.onabort = reject; FR.readAsArrayBuffer(file); - })); + }); }, clearForm() { @@ -390,9 +413,14 @@ }, deleteFiles(e) { - if (e) { e.stopPropagation(); } + if (e) { + e.stopPropagation(); + } this.clearForm(); - this.$input.wrap('
').parent('form').trigger('reset'); + this.$input + .wrap('') + .parent('form') + .trigger('reset'); this.$input.unwrap(); this.file = null; this.$input.trigger('change'); @@ -450,4 +478,4 @@ Whisper.FileInputView.makeImageThumbnail = makeImageThumbnail; Whisper.FileInputView.makeVideoThumbnail = makeVideoThumbnail; Whisper.FileInputView.makeVideoScreenshot = makeVideoScreenshot; -}()); +})(); diff --git a/js/views/group_member_list_view.js b/js/views/group_member_list_view.js index dc2e7c194..f3f678b14 100644 --- a/js/views/group_member_list_view.js +++ b/js/views/group_member_list_view.js @@ -1,40 +1,40 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - // TODO: take a title string which could replace the 'members' header - Whisper.GroupMemberList = Whisper.View.extend({ - className: 'group-member-list panel', - templateName: 'group-member-list', - initialize: function(options) { - this.needVerify = options.needVerify; + // TODO: take a title string which could replace the 'members' header + Whisper.GroupMemberList = Whisper.View.extend({ + className: 'group-member-list panel', + templateName: 'group-member-list', + initialize: function(options) { + this.needVerify = options.needVerify; - this.render(); + this.render(); - this.member_list_view = new Whisper.ContactListView({ - collection: this.model, - className: 'members', - toInclude: { - listenBack: options.listenBack - } - }); - this.member_list_view.render(); - - this.$('.container').append(this.member_list_view.el); + this.member_list_view = new Whisper.ContactListView({ + collection: this.model, + className: 'members', + toInclude: { + listenBack: options.listenBack, }, - render_attributes: function() { - var summary; - if (this.needVerify) { - summary = i18n('membersNeedingVerification'); - } + }); + this.member_list_view.render(); - return { - members: i18n('groupMembers'), - summary: summary - }; - } - }); + this.$('.container').append(this.member_list_view.el); + }, + render_attributes: function() { + var summary; + if (this.needVerify) { + summary = i18n('membersNeedingVerification'); + } + + return { + members: i18n('groupMembers'), + summary: summary, + }; + }, + }); })(); diff --git a/js/views/group_update_view.js b/js/views/group_update_view.js index 3d90171c1..17a8121a0 100644 --- a/js/views/group_update_view.js +++ b/js/views/group_update_view.js @@ -1,33 +1,32 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; +(function() { + 'use strict'; - window.Whisper = window.Whisper || {}; + window.Whisper = window.Whisper || {}; - Whisper.GroupUpdateView = Backbone.View.extend({ - tagName: "div", - className: "group-update", - render: function() { - //TODO l10n - if (this.model.left) { - this.$el.text(this.model.left + ' left the group'); - return this; - } + Whisper.GroupUpdateView = Backbone.View.extend({ + tagName: 'div', + className: 'group-update', + render: function() { + //TODO l10n + if (this.model.left) { + this.$el.text(this.model.left + ' left the group'); + return this; + } - var messages = ['Updated the group.']; - if (this.model.name) { - messages.push("Title is now '" + this.model.name + "'."); - } - if (this.model.joined) { - messages.push(this.model.joined.join(', ') + ' joined the group'); - } + var messages = ['Updated the group.']; + if (this.model.name) { + messages.push("Title is now '" + this.model.name + "'."); + } + if (this.model.joined) { + messages.push(this.model.joined.join(', ') + ' joined the group'); + } - this.$el.text(messages.join(' ')); - - return this; - } - }); + this.$el.text(messages.join(' ')); + return this; + }, + }); })(); diff --git a/js/views/hint_view.js b/js/views/hint_view.js index afefeffb8..6e1fa9b54 100644 --- a/js/views/hint_view.js +++ b/js/views/hint_view.js @@ -1,17 +1,17 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.HintView = Whisper.View.extend({ - templateName: 'hint', - initialize: function(options) { - this.content = options.content; - }, - render_attributes: function() { - return { content: this.content }; - } - }); + Whisper.HintView = Whisper.View.extend({ + templateName: 'hint', + initialize: function(options) { + this.content = options.content; + }, + render_attributes: function() { + return { content: this.content }; + }, + }); })(); diff --git a/js/views/identicon_svg_view.js b/js/views/identicon_svg_view.js index 822ca128f..07889a0b0 100644 --- a/js/views/identicon_svg_view.js +++ b/js/views/identicon_svg_view.js @@ -1,59 +1,60 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - /* + /* * Render an avatar identicon to an svg for use in a notification. */ - Whisper.IdenticonSVGView = Whisper.View.extend({ - templateName: 'identicon-svg', - initialize: function(options) { - this.render_attributes = options; - this.render_attributes.color = COLORS[this.render_attributes.color]; - }, - getSVGUrl: function() { - var html = this.render().$el.html(); - var svg = new Blob([html], {type: 'image/svg+xml;charset=utf-8'}); - return URL.createObjectURL(svg); - }, - getDataUrl: function() { - var svgurl = this.getSVGUrl(); - return new Promise(function(resolve) { - var img = document.createElement('img'); - img.onload = function () { - var canvas = loadImage.scale(img, { - canvas: true, maxWidth: 100, maxHeight: 100 - }); - var ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0); - URL.revokeObjectURL(svgurl); - resolve(canvas.toDataURL('image/png')); - }; + Whisper.IdenticonSVGView = Whisper.View.extend({ + templateName: 'identicon-svg', + initialize: function(options) { + this.render_attributes = options; + this.render_attributes.color = COLORS[this.render_attributes.color]; + }, + getSVGUrl: function() { + var html = this.render().$el.html(); + var svg = new Blob([html], { type: 'image/svg+xml;charset=utf-8' }); + return URL.createObjectURL(svg); + }, + getDataUrl: function() { + var svgurl = this.getSVGUrl(); + return new Promise(function(resolve) { + var img = document.createElement('img'); + img.onload = function() { + var canvas = loadImage.scale(img, { + canvas: true, + maxWidth: 100, + maxHeight: 100, + }); + var ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + URL.revokeObjectURL(svgurl); + resolve(canvas.toDataURL('image/png')); + }; - img.src = svgurl; - }); - } - }); - - var COLORS = { - red : '#EF5350', - pink : '#EC407A', - purple : '#AB47BC', - deep_purple : '#7E57C2', - indigo : '#5C6BC0', - blue : '#2196F3', - light_blue : '#03A9F4', - cyan : '#00BCD4', - teal : '#009688', - green : '#4CAF50', - light_green : '#7CB342', - orange : '#FF9800', - deep_orange : '#FF5722', - amber : '#FFB300', - blue_grey : '#607D8B' - }; + img.src = svgurl; + }); + }, + }); + var COLORS = { + red: '#EF5350', + pink: '#EC407A', + purple: '#AB47BC', + deep_purple: '#7E57C2', + indigo: '#5C6BC0', + blue: '#2196F3', + light_blue: '#03A9F4', + cyan: '#00BCD4', + teal: '#009688', + green: '#4CAF50', + light_green: '#7CB342', + orange: '#FF9800', + deep_orange: '#FF5722', + amber: '#FFB300', + blue_grey: '#607D8B', + }; })(); diff --git a/js/views/identity_key_send_error_view.js b/js/views/identity_key_send_error_view.js index 34fa19654..90572e7de 100644 --- a/js/views/identity_key_send_error_view.js +++ b/js/views/identity_key_send_error_view.js @@ -1,51 +1,54 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.IdentityKeySendErrorPanelView = Whisper.View.extend({ - className: 'identity-key-send-error panel', - templateName: 'identity-key-send-error', - initialize: function(options) { - this.listenBack = options.listenBack; - this.resetPanel = options.resetPanel; + Whisper.IdentityKeySendErrorPanelView = Whisper.View.extend({ + className: 'identity-key-send-error panel', + templateName: 'identity-key-send-error', + initialize: function(options) { + this.listenBack = options.listenBack; + this.resetPanel = options.resetPanel; - this.wasUnverified = this.model.isUnverified(); - this.listenTo(this.model, 'change', this.render); - }, - events: { - 'click .show-safety-number': 'showSafetyNumber', - 'click .send-anyway': 'sendAnyway', - 'click .cancel': 'cancel' - }, - showSafetyNumber: function() { - var view = new Whisper.KeyVerificationPanelView({ - model: this.model - }); - this.listenBack(view); - }, - sendAnyway: function() { - this.resetPanel(); - this.trigger('send-anyway'); - }, - cancel: function() { - this.resetPanel(); - }, - render_attributes: function() { - var send = i18n('sendAnyway'); - if (this.wasUnverified && !this.model.isUnverified()) { - send = i18n('resend'); - } + this.wasUnverified = this.model.isUnverified(); + this.listenTo(this.model, 'change', this.render); + }, + events: { + 'click .show-safety-number': 'showSafetyNumber', + 'click .send-anyway': 'sendAnyway', + 'click .cancel': 'cancel', + }, + showSafetyNumber: function() { + var view = new Whisper.KeyVerificationPanelView({ + model: this.model, + }); + this.listenBack(view); + }, + sendAnyway: function() { + this.resetPanel(); + this.trigger('send-anyway'); + }, + cancel: function() { + this.resetPanel(); + }, + render_attributes: function() { + var send = i18n('sendAnyway'); + if (this.wasUnverified && !this.model.isUnverified()) { + send = i18n('resend'); + } - var errorExplanation = i18n('identityKeyErrorOnSend', [this.model.getTitle(), this.model.getTitle()]); - return { - errorExplanation : errorExplanation, - showSafetyNumber : i18n('showSafetyNumber'), - sendAnyway : send, - cancel : i18n('cancel') - }; - } - }); + var errorExplanation = i18n('identityKeyErrorOnSend', [ + this.model.getTitle(), + this.model.getTitle(), + ]); + return { + errorExplanation: errorExplanation, + showSafetyNumber: i18n('showSafetyNumber'), + sendAnyway: send, + cancel: i18n('cancel'), + }; + }, + }); })(); diff --git a/js/views/import_view.js b/js/views/import_view.js index fe2bf6f8b..2a50e010e 100644 --- a/js/views/import_view.js +++ b/js/views/import_view.js @@ -1,7 +1,7 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { +(function() { 'use strict'; window.Whisper = window.Whisper || {}; @@ -36,7 +36,7 @@ }, reset: function() { return Whisper.Database.clear(); - } + }, }; Whisper.ImportView = Whisper.View.extend({ @@ -102,16 +102,19 @@ this.trigger('cancel'); }, onImport: function() { - window.Signal.Backup.getDirectoryForImport().then(function(directory) { - this.doImport(directory); - }.bind(this), function(error) { - if (error.name !== 'ChooseError') { - console.log( - 'Error choosing directory:', - error && error.stack ? error.stack : error - ); + window.Signal.Backup.getDirectoryForImport().then( + function(directory) { + this.doImport(directory); + }.bind(this), + function(error) { + if (error.name !== 'ChooseError') { + console.log( + 'Error choosing directory:', + error && error.stack ? error.stack : error + ); + } } - }); + ); }, onRegister: function() { // AppView listens for this, and opens up InstallView to the QR code step to @@ -127,53 +130,69 @@ this.render(); // Wait for prior database interaction to complete - this.pending = this.pending.then(function() { - // For resilience to interruption, clear database both before and on failure - return Whisper.Import.reset(); - }).then(function() { - return Promise.all([ - Whisper.Import.start(), - window.Signal.Backup.importFromDirectory(directory) - ]); - }).then(function(results) { - var importResult = results[1]; + this.pending = this.pending + .then(function() { + // For resilience to interruption, clear database both before and on failure + return Whisper.Import.reset(); + }) + .then(function() { + return Promise.all([ + Whisper.Import.start(), + window.Signal.Backup.importFromDirectory(directory), + ]); + }) + .then( + function(results) { + var importResult = results[1]; - // A full import changes so much we need a restart of the app - if (importResult.fullImport) { - return this.finishFullImport(directory); - } + // A full import changes so much we need a restart of the app + if (importResult.fullImport) { + return this.finishFullImport(directory); + } - // A light import just brings in contacts, groups, and messages. And we need a - // normal link to finish the process. - return this.finishLightImport(directory); - }.bind(this)).catch(function(error) { - console.log('Error importing:', error && error.stack ? error.stack : error); + // A light import just brings in contacts, groups, and messages. And we need a + // normal link to finish the process. + return this.finishLightImport(directory); + }.bind(this) + ) + .catch( + function(error) { + console.log( + 'Error importing:', + error && error.stack ? error.stack : error + ); - this.error = error || new Error('Something went wrong!'); - this.state = null; - this.render(); + this.error = error || new Error('Something went wrong!'); + this.state = null; + this.render(); - return Whisper.Import.reset(); - }.bind(this)); + return Whisper.Import.reset(); + }.bind(this) + ); }, finishLightImport: function(directory) { ConversationController.reset(); - return ConversationController.load().then(function() { - return Promise.all([ + return ConversationController.load() + .then(function() { + return Promise.all([ Whisper.Import.saveLocation(directory), Whisper.Import.complete(), ]); - }).then(function() { - this.state = State.LIGHT_COMPLETE; - this.render(); - }.bind(this)); + }) + .then( + function() { + this.state = State.LIGHT_COMPLETE; + this.render(); + }.bind(this) + ); }, finishFullImport: function(directory) { // Catching in-memory cache up with what's in indexeddb now... // NOTE: this fires storage.onready, listened to across the app. We'll restart // to complete the install to start up cleanly with everything now in the DB. - return storage.fetch() + return storage + .fetch() .then(function() { return Promise.all([ // Clearing any migration-related state inherited from the Chrome App @@ -183,12 +202,15 @@ storage.remove('migrationStorageLocation'), Whisper.Import.saveLocation(directory), - Whisper.Import.complete() + Whisper.Import.complete(), ]); - }).then(function() { - this.state = State.COMPLETE; - this.render(); - }.bind(this)); - } + }) + .then( + function() { + this.state = State.COMPLETE; + this.render(); + }.bind(this) + ); + }, }); })(); diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index cd759ee29..987814294 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -5,7 +5,7 @@ /* global Whisper: false */ // eslint-disable-next-line func-names -(function () { +(function() { 'use strict'; window.Whisper = window.Whisper || {}; @@ -15,9 +15,12 @@ open(conversation) { const id = `conversation-${conversation.cid}`; if (id !== this.el.firstChild.id) { - this.$el.first().find('video, audio').each(function pauseMedia() { - this.pause(); - }); + this.$el + .first() + .find('video, audio') + .each(function pauseMedia() { + this.pause(); + }); let $el = this.$(`#${id}`); if ($el === null || $el.length === 0) { const view = new Whisper.ConversationView({ @@ -65,7 +68,6 @@ }, }); - Whisper.AppLoadingScreen = Whisper.View.extend({ templateName: 'app-loading-screen', className: 'app-loading-screen', @@ -147,7 +149,8 @@ ); this.networkStatusView = new Whisper.NetworkStatusView(); - this.$el.find('.network-status-container') + this.$el + .find('.network-status-container') .append(this.networkStatusView.render().el); extension.windows.onClosed(() => { @@ -194,7 +197,8 @@ default: console.log( 'Whisper.InboxView::startConnectionListener:', - 'Unknown web socket status:', status + 'Unknown web socket status:', + status ); break; } @@ -254,7 +258,9 @@ openConversation(e, conversation) { this.searchView.hideHints(); if (conversation) { - this.conversation_stack.open(ConversationController.get(conversation.id)); + this.conversation_stack.open( + ConversationController.get(conversation.id) + ); this.focusConversation(); } }, @@ -279,4 +285,4 @@ }; }, }); -}()); +})(); diff --git a/js/views/install_view.js b/js/views/install_view.js index 9840f19da..4d8575cb1 100644 --- a/js/views/install_view.js +++ b/js/views/install_view.js @@ -1,196 +1,204 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - var Steps = { - INSTALL_SIGNAL: 2, - SCAN_QR_CODE: 3, - ENTER_NAME: 4, - PROGRESS_BAR: 5, - TOO_MANY_DEVICES: 'TooManyDevices', - NETWORK_ERROR: 'NetworkError', - }; + var Steps = { + INSTALL_SIGNAL: 2, + SCAN_QR_CODE: 3, + ENTER_NAME: 4, + PROGRESS_BAR: 5, + TOO_MANY_DEVICES: 'TooManyDevices', + NETWORK_ERROR: 'NetworkError', + }; - var DEVICE_NAME_SELECTOR = 'input.device-name'; - var CONNECTION_ERROR = -1; - var TOO_MANY_DEVICES = 411; + var DEVICE_NAME_SELECTOR = 'input.device-name'; + var CONNECTION_ERROR = -1; + var TOO_MANY_DEVICES = 411; - Whisper.InstallView = Whisper.View.extend({ - templateName: 'link-flow-template', - className: 'main full-screen-flow', - events: { - 'click .try-again': 'connect', - 'click .finish': 'finishLinking', - // the actual next step happens in confirmNumber() on submit form #link-phone - }, - initialize: function(options) { - options = options || {}; + Whisper.InstallView = Whisper.View.extend({ + templateName: 'link-flow-template', + className: 'main full-screen-flow', + events: { + 'click .try-again': 'connect', + 'click .finish': 'finishLinking', + // the actual next step happens in confirmNumber() on submit form #link-phone + }, + initialize: function(options) { + options = options || {}; - this.selectStep(Steps.SCAN_QR_CODE); - this.connect(); - this.on('disconnected', this.reconnect); + this.selectStep(Steps.SCAN_QR_CODE); + this.connect(); + this.on('disconnected', this.reconnect); - // Keep data around if it's a re-link, or the middle of a light import - this.shouldRetainData = Whisper.Registration.everDone() || options.hasExistingData; - }, - render_attributes: function() { - var errorMessage; + // Keep data around if it's a re-link, or the middle of a light import + this.shouldRetainData = + Whisper.Registration.everDone() || options.hasExistingData; + }, + render_attributes: function() { + var errorMessage; - if (this.error) { - if (this.error.name === 'HTTPError' - && this.error.code == TOO_MANY_DEVICES) { + if (this.error) { + if ( + this.error.name === 'HTTPError' && + this.error.code == TOO_MANY_DEVICES + ) { + errorMessage = i18n('installTooManyDevices'); + } else if ( + this.error.name === 'HTTPError' && + this.error.code == CONNECTION_ERROR + ) { + errorMessage = i18n('installConnectionFailed'); + } else if (this.error.message === 'websocket closed') { + // AccountManager.registerSecondDevice uses this specific + // 'websocket closed' error message + errorMessage = i18n('installConnectionFailed'); + } - errorMessage = i18n('installTooManyDevices'); - } - else if (this.error.name === 'HTTPError' - && this.error.code == CONNECTION_ERROR) { + return { + isError: true, + errorHeader: 'Something went wrong!', + errorMessage, + errorButton: 'Try again', + }; + } - errorMessage = i18n('installConnectionFailed'); - } - else if (this.error.message === 'websocket closed') { - // AccountManager.registerSecondDevice uses this specific - // 'websocket closed' error message - errorMessage = i18n('installConnectionFailed'); - } + return { + isStep3: this.step === Steps.SCAN_QR_CODE, + linkYourPhone: i18n('linkYourPhone'), + signalSettings: i18n('signalSettings'), + linkedDevices: i18n('linkedDevices'), + androidFinalStep: i18n('plusButton'), + appleFinalStep: i18n('linkNewDevice'), - return { - isError: true, - errorHeader: 'Something went wrong!', - errorMessage, - errorButton: 'Try again', - }; - } + isStep4: this.step === Steps.ENTER_NAME, + chooseName: i18n('chooseDeviceName'), + finishLinkingPhoneButton: i18n('finishLinkingPhone'), - return { - isStep3: this.step === Steps.SCAN_QR_CODE, - linkYourPhone: i18n('linkYourPhone'), - signalSettings: i18n('signalSettings'), - linkedDevices: i18n('linkedDevices'), - androidFinalStep: i18n('plusButton'), - appleFinalStep: i18n('linkNewDevice'), + isStep5: this.step === Steps.PROGRESS_BAR, + syncing: i18n('initialSync'), + }; + }, + selectStep: function(step) { + this.step = step; + this.render(); + }, + connect: function() { + this.error = null; + this.selectStep(Steps.SCAN_QR_CODE); + this.clearQR(); + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } - isStep4: this.step === Steps.ENTER_NAME, - chooseName: i18n('chooseDeviceName'), - finishLinkingPhoneButton: i18n('finishLinkingPhone'), + var accountManager = getAccountManager(); - isStep5: this.step === Steps.PROGRESS_BAR, - syncing: i18n('initialSync'), - }; - }, - selectStep: function(step) { - this.step = step; - this.render(); - }, - connect: function() { - this.error = null; - this.selectStep(Steps.SCAN_QR_CODE); - this.clearQR(); - if (this.timeout) { - clearTimeout(this.timeout); - this.timeout = null; - } + accountManager + .registerSecondDevice( + this.setProvisioningUrl.bind(this), + this.confirmNumber.bind(this) + ) + .catch(this.handleDisconnect.bind(this)); + }, + handleDisconnect: function(e) { + console.log('provisioning failed', e.stack); - var accountManager = getAccountManager(); + this.error = e; + this.render(); - accountManager.registerSecondDevice( - this.setProvisioningUrl.bind(this), - this.confirmNumber.bind(this) - ).catch(this.handleDisconnect.bind(this)); - }, - handleDisconnect: function(e) { - console.log('provisioning failed', e.stack); + if (e.message === 'websocket closed') { + this.trigger('disconnected'); + } else if ( + e.name !== 'HTTPError' || + (e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES) + ) { + throw e; + } + }, + reconnect: function() { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } + this.timeout = setTimeout(this.connect.bind(this), 10000); + }, + clearQR: function() { + this.$('#qr img').remove(); + this.$('#qr canvas').remove(); + this.$('#qr .container').show(); + this.$('#qr').removeClass('ready'); + }, + setProvisioningUrl: function(url) { + if ($('#qr').length === 0) { + console.log('Did not find #qr element in the DOM!'); + return; + } - this.error = e; - this.render(); + this.$('#qr .container').hide(); + this.qr = new QRCode(this.$('#qr')[0]).makeCode(url); + this.$('#qr').removeAttr('title'); + this.$('#qr').addClass('ready'); + }, + setDeviceNameDefault: function() { + var deviceName = textsecure.storage.user.getDeviceName(); - if (e.message === 'websocket closed') { - this.trigger('disconnected'); - } else if (e.name !== 'HTTPError' - || (e.code !== CONNECTION_ERROR && e.code !== TOO_MANY_DEVICES)) { + this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.config.hostname); + this.$(DEVICE_NAME_SELECTOR).focus(); + }, + finishLinking: function() { + // We use a form so we get submit-on-enter behavior + this.$('#link-phone').submit(); + }, + confirmNumber: function(number) { + var tsp = textsecure.storage.protocol; - throw e; - } - }, - reconnect: function() { - if (this.timeout) { - clearTimeout(this.timeout); - this.timeout = null; - } - this.timeout = setTimeout(this.connect.bind(this), 10000); - }, - clearQR: function() { - this.$('#qr img').remove(); - this.$('#qr canvas').remove(); - this.$('#qr .container').show(); - this.$('#qr').removeClass('ready'); - }, - setProvisioningUrl: function(url) { - if ($('#qr').length === 0) { - console.log('Did not find #qr element in the DOM!'); + window.removeSetupMenuItems(); + this.selectStep(Steps.ENTER_NAME); + this.setDeviceNameDefault(); + + return new Promise( + function(resolve, reject) { + this.$('#link-phone').submit( + function(e) { + e.stopPropagation(); + e.preventDefault(); + + var name = this.$(DEVICE_NAME_SELECTOR).val(); + name = name.replace(/\0/g, ''); // strip unicode null + if (name.trim().length === 0) { + this.$(DEVICE_NAME_SELECTOR).focus(); return; - } + } - this.$('#qr .container').hide(); - this.qr = new QRCode(this.$('#qr')[0]).makeCode(url); - this.$('#qr').removeAttr('title'); - this.$('#qr').addClass('ready'); - }, - setDeviceNameDefault: function() { - var deviceName = textsecure.storage.user.getDeviceName(); + this.selectStep(Steps.PROGRESS_BAR); - this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.config.hostname); - this.$(DEVICE_NAME_SELECTOR).focus(); - }, - finishLinking: function() { - // We use a form so we get submit-on-enter behavior - this.$('#link-phone').submit(); - }, - confirmNumber: function(number) { - var tsp = textsecure.storage.protocol; + var finish = function() { + resolve(name); + }; - window.removeSetupMenuItems(); - this.selectStep(Steps.ENTER_NAME); - this.setDeviceNameDefault(); + // Delete all data from database unless we're in the middle + // of a re-link, or we are finishing a light import. Without this, + // app restarts at certain times can cause weird things to happen, + // like data from a previous incomplete light import showing up + // after a new install. + if (this.shouldRetainData) { + return finish(); + } - return new Promise(function(resolve, reject) { - this.$('#link-phone').submit(function(e) { - e.stopPropagation(); - e.preventDefault(); - - var name = this.$(DEVICE_NAME_SELECTOR).val(); - name = name.replace(/\0/g,''); // strip unicode null - if (name.trim().length === 0) { - this.$(DEVICE_NAME_SELECTOR).focus(); - return; - } - - this.selectStep(Steps.PROGRESS_BAR); - - var finish = function() { - resolve(name); - }; - - // Delete all data from database unless we're in the middle - // of a re-link, or we are finishing a light import. Without this, - // app restarts at certain times can cause weird things to happen, - // like data from a previous incomplete light import showing up - // after a new install. - if (this.shouldRetainData) { - return finish(); - } - - tsp.removeAllData().then(finish, function(error) { - console.log( - 'confirmNumber: error clearing database', - error && error.stack ? error.stack : error - ); - finish(); - }); - }.bind(this)); - }.bind(this)); - }, - }); + tsp.removeAllData().then(finish, function(error) { + console.log( + 'confirmNumber: error clearing database', + error && error.stack ? error.stack : error + ); + finish(); + }); + }.bind(this) + ); + }.bind(this) + ); + }, + }); })(); diff --git a/js/views/key_verification_view.js b/js/views/key_verification_view.js index a8aeca713..949c26a49 100644 --- a/js/views/key_verification_view.js +++ b/js/views/key_verification_view.js @@ -1,121 +1,138 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.KeyVerificationPanelView = Whisper.View.extend({ - className: 'key-verification panel', - templateName: 'key-verification', - events: { - 'click button.verify': 'toggleVerified', - }, - initialize: function(options) { - this.ourNumber = textsecure.storage.user.getNumber(); - if (options.newKey) { - this.theirKey = options.newKey; + Whisper.KeyVerificationPanelView = Whisper.View.extend({ + className: 'key-verification panel', + templateName: 'key-verification', + events: { + 'click button.verify': 'toggleVerified', + }, + initialize: function(options) { + this.ourNumber = textsecure.storage.user.getNumber(); + if (options.newKey) { + this.theirKey = options.newKey; + } + + this.loadKeys().then( + function() { + this.listenTo(this.model, 'change', this.render); + }.bind(this) + ); + }, + loadKeys: function() { + return Promise.all([this.loadTheirKey(), this.loadOurKey()]) + .then(this.generateSecurityNumber.bind(this)) + .then(this.render.bind(this)); + //.then(this.makeQRCode.bind(this)); + }, + makeQRCode: function() { + // Per Lilia: We can't turn this on until it generates a Latin1 string, as is + // required by the mobile clients. + new QRCode(this.$('.qr')[0]).makeCode( + dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64') + ); + }, + loadTheirKey: function() { + return textsecure.storage.protocol.loadIdentityKey(this.model.id).then( + function(theirKey) { + this.theirKey = theirKey; + }.bind(this) + ); + }, + loadOurKey: function() { + return textsecure.storage.protocol.loadIdentityKey(this.ourNumber).then( + function(ourKey) { + this.ourKey = ourKey; + }.bind(this) + ); + }, + generateSecurityNumber: function() { + return new libsignal.FingerprintGenerator(5200) + .createFor(this.ourNumber, this.ourKey, this.model.id, this.theirKey) + .then( + function(securityNumber) { + this.securityNumber = securityNumber; + }.bind(this) + ); + }, + onSafetyNumberChanged: function() { + this.model.getProfiles().then(this.loadKeys.bind(this)); + + var dialog = new Whisper.ConfirmationDialogView({ + message: i18n('changedRightAfterVerify', [ + this.model.getTitle(), + this.model.getTitle(), + ]), + hideCancel: true, + }); + + dialog.$el.insertBefore(this.el); + dialog.focusCancel(); + }, + toggleVerified: function() { + this.$('button.verify').attr('disabled', true); + this.model + .toggleVerified() + .catch( + function(result) { + if (result instanceof Error) { + if (result.name === 'OutgoingIdentityKeyError') { + this.onSafetyNumberChanged(); + } else { + console.log('failed to toggle verified:', result.stack); + } + } else { + var keyError = _.some(result.errors, function(error) { + return error.name === 'OutgoingIdentityKeyError'; + }); + if (keyError) { + this.onSafetyNumberChanged(); + } else { + _.forEach(result.errors, function(error) { + console.log('failed to toggle verified:', error.stack); + }); + } } + }.bind(this) + ) + .then( + function() { + this.$('button.verify').removeAttr('disabled'); + }.bind(this) + ); + }, + render_attributes: function() { + var s = this.securityNumber; + var chunks = []; + for (var i = 0; i < s.length; i += 5) { + chunks.push(s.substring(i, i + 5)); + } + var name = this.model.getTitle(); + var yourSafetyNumberWith = i18n('yourSafetyNumberWith', name); + var isVerified = this.model.isVerified(); + var verifyButton = isVerified ? i18n('unverify') : i18n('verify'); + var verifiedStatus = isVerified + ? i18n('isVerified', name) + : i18n('isNotVerified', name); - this.loadKeys().then(function() { - this.listenTo(this.model, 'change', this.render); - }.bind(this)); - }, - loadKeys: function() { - return Promise.all([ - this.loadTheirKey(), - this.loadOurKey(), - ]).then(this.generateSecurityNumber.bind(this)) - .then(this.render.bind(this)); - //.then(this.makeQRCode.bind(this)); - }, - makeQRCode: function() { - // Per Lilia: We can't turn this on until it generates a Latin1 string, as is - // required by the mobile clients. - new QRCode(this.$('.qr')[0]).makeCode( - dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64') - ); - }, - loadTheirKey: function() { - return textsecure.storage.protocol.loadIdentityKey( - this.model.id - ).then(function(theirKey) { - this.theirKey = theirKey; - }.bind(this)); - }, - loadOurKey: function() { - return textsecure.storage.protocol.loadIdentityKey( - this.ourNumber - ).then(function(ourKey) { - this.ourKey = ourKey; - }.bind(this)); - }, - generateSecurityNumber: function() { - return new libsignal.FingerprintGenerator(5200).createFor( - this.ourNumber, this.ourKey, this.model.id, this.theirKey - ).then(function(securityNumber) { - this.securityNumber = securityNumber; - }.bind(this)); - }, - onSafetyNumberChanged: function() { - this.model.getProfiles().then(this.loadKeys.bind(this)); - - var dialog = new Whisper.ConfirmationDialogView({ - message: i18n('changedRightAfterVerify', [this.model.getTitle(), this.model.getTitle()]), - hideCancel: true - }); - - dialog.$el.insertBefore(this.el); - dialog.focusCancel(); - }, - toggleVerified: function() { - this.$('button.verify').attr('disabled', true); - this.model.toggleVerified().catch(function(result) { - if (result instanceof Error) { - if (result.name === 'OutgoingIdentityKeyError') { - this.onSafetyNumberChanged(); - } else { - console.log('failed to toggle verified:', result.stack); - } - } else { - var keyError = _.some(result.errors, function(error) { - return error.name === 'OutgoingIdentityKeyError'; - }); - if (keyError) { - this.onSafetyNumberChanged(); - } else { - _.forEach(result.errors, function(error) { - console.log('failed to toggle verified:', error.stack); - }); - } - } - }.bind(this)).then(function() { - this.$('button.verify').removeAttr('disabled'); - }.bind(this)); - }, - render_attributes: function() { - var s = this.securityNumber; - var chunks = []; - for (var i = 0; i < s.length; i += 5) { - chunks.push(s.substring(i, i+5)); - } - var name = this.model.getTitle(); - var yourSafetyNumberWith = i18n('yourSafetyNumberWith', name); - var isVerified = this.model.isVerified(); - var verifyButton = isVerified ? i18n('unverify') : i18n('verify'); - var verifiedStatus = isVerified ? i18n('isVerified', name) : i18n('isNotVerified', name); - - return { - learnMore : i18n('learnMore'), - theirKeyUnknown : i18n('theirIdentityUnknown'), - yourSafetyNumberWith : i18n('yourSafetyNumberWith', this.model.getTitle()), - verifyHelp : i18n('verifyHelp', this.model.getTitle()), - verifyButton : verifyButton, - hasTheirKey : this.theirKey !== undefined, - chunks : chunks, - isVerified : isVerified, - verifiedStatus : verifiedStatus - }; - } - }); + return { + learnMore: i18n('learnMore'), + theirKeyUnknown: i18n('theirIdentityUnknown'), + yourSafetyNumberWith: i18n( + 'yourSafetyNumberWith', + this.model.getTitle() + ), + verifyHelp: i18n('verifyHelp', this.model.getTitle()), + verifyButton: verifyButton, + hasTheirKey: this.theirKey !== undefined, + chunks: chunks, + isVerified: isVerified, + verifiedStatus: verifiedStatus, + }; + }, + }); })(); diff --git a/js/views/last_seen_indicator_view.js b/js/views/last_seen_indicator_view.js index f1b574d1f..9637e9dbb 100644 --- a/js/views/last_seen_indicator_view.js +++ b/js/views/last_seen_indicator_view.js @@ -1,36 +1,38 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - var FIVE_SECONDS = 5 * 1000; + var FIVE_SECONDS = 5 * 1000; - Whisper.LastSeenIndicatorView = Whisper.View.extend({ - className: 'last-seen-indicator-view', - templateName: 'last-seen-indicator-view', - initialize: function(options) { - options = options || {}; - this.count = options.count || 0; - }, + Whisper.LastSeenIndicatorView = Whisper.View.extend({ + className: 'last-seen-indicator-view', + templateName: 'last-seen-indicator-view', + initialize: function(options) { + options = options || {}; + this.count = options.count || 0; + }, - increment: function(count) { - this.count += count; - this.render(); - }, + increment: function(count) { + this.count += count; + this.render(); + }, - getCount: function() { - return this.count; - }, + getCount: function() { + return this.count; + }, - render_attributes: function() { - var unreadMessages = this.count === 1 ? i18n('unreadMessage') - : i18n('unreadMessages', [this.count]); + render_attributes: function() { + var unreadMessages = + this.count === 1 + ? i18n('unreadMessage') + : i18n('unreadMessages', [this.count]); - return { - unreadMessages: unreadMessages - }; - } - }); + return { + unreadMessages: unreadMessages, + }; + }, + }); })(); diff --git a/js/views/list_view.js b/js/views/list_view.js index 394d9ed41..80c29377e 100644 --- a/js/views/list_view.js +++ b/js/views/list_view.js @@ -1,40 +1,40 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - /* + /* * Generic list view that watches a given collection, wraps its members in * a given child view and adds the child view elements to its own element. */ - Whisper.ListView = Backbone.View.extend({ - tagName: 'ul', - itemView: Backbone.View, - initialize: function(options) { - this.options = options || {}; - this.listenTo(this.collection, 'add', this.addOne); - this.listenTo(this.collection, 'reset', this.addAll); - }, + Whisper.ListView = Backbone.View.extend({ + tagName: 'ul', + itemView: Backbone.View, + initialize: function(options) { + this.options = options || {}; + this.listenTo(this.collection, 'add', this.addOne); + this.listenTo(this.collection, 'reset', this.addAll); + }, - addOne: function(model) { - if (this.itemView) { - var options = _.extend({}, this.options.toInclude, {model: model}); - var view = new this.itemView(options); - this.$el.append(view.render().el); - this.$el.trigger('add'); - } - }, + addOne: function(model) { + if (this.itemView) { + var options = _.extend({}, this.options.toInclude, { model: model }); + var view = new this.itemView(options); + this.$el.append(view.render().el); + this.$el.trigger('add'); + } + }, - addAll: function() { - this.$el.html(''); - this.collection.each(this.addOne, this); - }, + addAll: function() { + this.$el.html(''); + this.collection.each(this.addOne, this); + }, - render: function() { - this.addAll(); - return this; - } - }); + render: function() { + this.addAll(); + return this; + }, + }); })(); diff --git a/js/views/message_detail_view.js b/js/views/message_detail_view.js index 0de79ec43..8cbb8c419 100644 --- a/js/views/message_detail_view.js +++ b/js/views/message_detail_view.js @@ -1,168 +1,193 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - var ContactView = Whisper.View.extend({ - className: 'contact-detail', - templateName: 'contact-detail', - initialize: function(options) { - this.listenBack = options.listenBack; - this.resetPanel = options.resetPanel; - this.message = options.message; + var ContactView = Whisper.View.extend({ + className: 'contact-detail', + templateName: 'contact-detail', + initialize: function(options) { + this.listenBack = options.listenBack; + this.resetPanel = options.resetPanel; + this.message = options.message; - var newIdentity = i18n('newIdentity'); - this.errors = _.map(options.errors, function(error) { - if (error.name === 'OutgoingIdentityKeyError') { - error.message = newIdentity; - } - return error; - }); - this.outgoingKeyError = _.find(this.errors, function(error) { - return error.name === 'OutgoingIdentityKeyError'; - }); - }, - events: { - 'click': 'onClick' - }, - onClick: function() { - if (this.outgoingKeyError) { - var view = new Whisper.IdentityKeySendErrorPanelView({ - model: this.model, - listenBack: this.listenBack, - resetPanel: this.resetPanel - }); - - this.listenTo(view, 'send-anyway', this.onSendAnyway); - - view.render(); - - this.listenBack(view); - view.$('.cancel').focus(); - } - }, - forceSend: function() { - this.model.updateVerified().then(function() { - if (this.model.isUnverified()) { - return this.model.setVerifiedDefault(); - } - }.bind(this)).then(function() { - return this.model.isUntrusted(); - }.bind(this)).then(function(untrusted) { - if (untrusted) { - return this.model.setApproved(); - } - }.bind(this)).then(function() { - this.message.resend(this.outgoingKeyError.number); - }.bind(this)); - }, - onSendAnyway: function() { - if (this.outgoingKeyError) { - this.forceSend(); - } - }, - render_attributes: function() { - var showButton = Boolean(this.outgoingKeyError); - - return { - status : this.message.getStatus(this.model.id), - name : this.model.getTitle(), - avatar : this.model.getAvatar(), - errors : this.errors, - showErrorButton : showButton, - errorButtonLabel : i18n('view') - }; + var newIdentity = i18n('newIdentity'); + this.errors = _.map(options.errors, function(error) { + if (error.name === 'OutgoingIdentityKeyError') { + error.message = newIdentity; } - }); + return error; + }); + this.outgoingKeyError = _.find(this.errors, function(error) { + return error.name === 'OutgoingIdentityKeyError'; + }); + }, + events: { + click: 'onClick', + }, + onClick: function() { + if (this.outgoingKeyError) { + var view = new Whisper.IdentityKeySendErrorPanelView({ + model: this.model, + listenBack: this.listenBack, + resetPanel: this.resetPanel, + }); - Whisper.MessageDetailView = Whisper.View.extend({ - className: 'message-detail panel', - templateName: 'message-detail', - initialize: function(options) { - this.listenBack = options.listenBack; - this.resetPanel = options.resetPanel; + this.listenTo(view, 'send-anyway', this.onSendAnyway); - this.view = new Whisper.MessageView({model: this.model}); - this.view.render(); - this.conversation = options.conversation; + view.render(); - this.listenTo(this.model, 'change', this.render); - }, - events: { - 'click button.delete': 'onDelete' - }, - onDelete: function() { - var dialog = new Whisper.ConfirmationDialogView({ - message: i18n('deleteWarning'), - okText: i18n('delete'), - resolve: function() { - this.model.destroy(); - this.resetPanel(); - }.bind(this) - }); - - this.$el.prepend(dialog.el); - dialog.focusCancel(); - }, - getContacts: function() { - // Return the set of models to be rendered in this view - var ids; - if (this.model.isIncoming()) { - ids = [ this.model.get('source') ]; - } else if (this.model.isOutgoing()) { - ids = this.model.get('recipients'); - if (!ids) { - // older messages have no recipients field - // use the current set of recipients - ids = this.conversation.getRecipients(); - } + this.listenBack(view); + view.$('.cancel').focus(); + } + }, + forceSend: function() { + this.model + .updateVerified() + .then( + function() { + if (this.model.isUnverified()) { + return this.model.setVerifiedDefault(); } - return Promise.all(ids.map(function(number) { - return ConversationController.getOrCreateAndWait(number, 'private'); - })); - }, - renderContact: function(contact) { - var view = new ContactView({ - model: contact, - errors: this.grouped[contact.id], - listenBack: this.listenBack, - resetPanel: this.resetPanel, - message: this.model - }).render(); - this.$('.contacts').append(view.el); - }, - render: function() { - var errorsWithoutNumber = _.reject(this.model.get('errors'), function(error) { - return Boolean(error.number); - }); + }.bind(this) + ) + .then( + function() { + return this.model.isUntrusted(); + }.bind(this) + ) + .then( + function(untrusted) { + if (untrusted) { + return this.model.setApproved(); + } + }.bind(this) + ) + .then( + function() { + this.message.resend(this.outgoingKeyError.number); + }.bind(this) + ); + }, + onSendAnyway: function() { + if (this.outgoingKeyError) { + this.forceSend(); + } + }, + render_attributes: function() { + var showButton = Boolean(this.outgoingKeyError); - this.$el.html(Mustache.render(_.result(this, 'template', ''), { - sent_at : moment(this.model.get('sent_at')).format('LLLL'), - received_at : this.model.isIncoming() ? moment(this.model.get('received_at')).format('LLLL') : null, - tofrom : this.model.isIncoming() ? i18n('from') : i18n('to'), - errors : errorsWithoutNumber, - title : i18n('messageDetail'), - sent : i18n('sent'), - received : i18n('received'), - errorLabel : i18n('error'), - deleteLabel : i18n('deleteMessage'), - retryDescription: i18n('retryDescription') - })); - this.view.$el.prependTo(this.$('.message-container')); + return { + status: this.message.getStatus(this.model.id), + name: this.model.getTitle(), + avatar: this.model.getAvatar(), + errors: this.errors, + showErrorButton: showButton, + errorButtonLabel: i18n('view'), + }; + }, + }); - this.grouped = _.groupBy(this.model.get('errors'), 'number'); + Whisper.MessageDetailView = Whisper.View.extend({ + className: 'message-detail panel', + templateName: 'message-detail', + initialize: function(options) { + this.listenBack = options.listenBack; + this.resetPanel = options.resetPanel; - this.getContacts().then(function(contacts) { - _.sortBy(contacts, function(c) { - var prefix = this.grouped[c.id] ? '0' : '1'; - // this prefix ensures that contacts with errors are listed first; - // otherwise it's alphabetical - return prefix + c.getTitle(); - }.bind(this)).forEach(this.renderContact.bind(this)); - }.bind(this)); + this.view = new Whisper.MessageView({ model: this.model }); + this.view.render(); + this.conversation = options.conversation; + + this.listenTo(this.model, 'change', this.render); + }, + events: { + 'click button.delete': 'onDelete', + }, + onDelete: function() { + var dialog = new Whisper.ConfirmationDialogView({ + message: i18n('deleteWarning'), + okText: i18n('delete'), + resolve: function() { + this.model.destroy(); + this.resetPanel(); + }.bind(this), + }); + + this.$el.prepend(dialog.el); + dialog.focusCancel(); + }, + getContacts: function() { + // Return the set of models to be rendered in this view + var ids; + if (this.model.isIncoming()) { + ids = [this.model.get('source')]; + } else if (this.model.isOutgoing()) { + ids = this.model.get('recipients'); + if (!ids) { + // older messages have no recipients field + // use the current set of recipients + ids = this.conversation.getRecipients(); } - }); + } + return Promise.all( + ids.map(function(number) { + return ConversationController.getOrCreateAndWait(number, 'private'); + }) + ); + }, + renderContact: function(contact) { + var view = new ContactView({ + model: contact, + errors: this.grouped[contact.id], + listenBack: this.listenBack, + resetPanel: this.resetPanel, + message: this.model, + }).render(); + this.$('.contacts').append(view.el); + }, + render: function() { + var errorsWithoutNumber = _.reject(this.model.get('errors'), function( + error + ) { + return Boolean(error.number); + }); + this.$el.html( + Mustache.render(_.result(this, 'template', ''), { + sent_at: moment(this.model.get('sent_at')).format('LLLL'), + received_at: this.model.isIncoming() + ? moment(this.model.get('received_at')).format('LLLL') + : null, + tofrom: this.model.isIncoming() ? i18n('from') : i18n('to'), + errors: errorsWithoutNumber, + title: i18n('messageDetail'), + sent: i18n('sent'), + received: i18n('received'), + errorLabel: i18n('error'), + deleteLabel: i18n('deleteMessage'), + retryDescription: i18n('retryDescription'), + }) + ); + this.view.$el.prependTo(this.$('.message-container')); + + this.grouped = _.groupBy(this.model.get('errors'), 'number'); + + this.getContacts().then( + function(contacts) { + _.sortBy( + contacts, + function(c) { + var prefix = this.grouped[c.id] ? '0' : '1'; + // this prefix ensures that contacts with errors are listed first; + // otherwise it's alphabetical + return prefix + c.getTitle(); + }.bind(this) + ).forEach(this.renderContact.bind(this)); + }.bind(this) + ); + }, + }); })(); diff --git a/js/views/message_list_view.js b/js/views/message_list_view.js index f39506fe3..a659ce26b 100644 --- a/js/views/message_list_view.js +++ b/js/views/message_list_view.js @@ -1,119 +1,123 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.MessageListView = Whisper.ListView.extend({ - tagName: 'ul', - className: 'message-list', - itemView: Whisper.MessageView, - events: { - 'scroll': 'onScroll', - }, - initialize: function() { - Whisper.ListView.prototype.initialize.call(this); + Whisper.MessageListView = Whisper.ListView.extend({ + tagName: 'ul', + className: 'message-list', + itemView: Whisper.MessageView, + events: { + scroll: 'onScroll', + }, + initialize: function() { + Whisper.ListView.prototype.initialize.call(this); - this.triggerLazyScroll = _.debounce(function() { - this.$el.trigger('lazyScroll'); - }.bind(this), 500); - }, - onScroll: function() { - this.measureScrollPosition(); - if (this.$el.scrollTop() === 0) { - this.$el.trigger('loadMore'); - } - if (this.atBottom()) { - this.$el.trigger('atBottom'); - } else if (this.bottomOffset > this.outerHeight) { - this.$el.trigger('farFromBottom'); - } + this.triggerLazyScroll = _.debounce( + function() { + this.$el.trigger('lazyScroll'); + }.bind(this), + 500 + ); + }, + onScroll: function() { + this.measureScrollPosition(); + if (this.$el.scrollTop() === 0) { + this.$el.trigger('loadMore'); + } + if (this.atBottom()) { + this.$el.trigger('atBottom'); + } else if (this.bottomOffset > this.outerHeight) { + this.$el.trigger('farFromBottom'); + } - this.triggerLazyScroll(); - }, - atBottom: function() { - return this.bottomOffset < 30; - }, - measureScrollPosition: function() { - if (this.el.scrollHeight === 0) { // hidden - return; - } - this.outerHeight = this.$el.outerHeight(); - this.scrollPosition = this.$el.scrollTop() + this.outerHeight; - this.scrollHeight = this.el.scrollHeight; - this.bottomOffset = this.scrollHeight - this.scrollPosition; - }, - resetScrollPosition: function() { - this.$el.scrollTop(this.scrollPosition - this.$el.outerHeight()); - }, - scrollToBottomIfNeeded: function() { - // This is counter-intuitive. Our current bottomOffset is reflective of what - // we last measured, not necessarily the current state. And this is called - // after we just made a change to the DOM: inserting a message, or an image - // finished loading. So if we were near the bottom before, we _need_ to be - // at the bottom again. So we scroll to the bottom. - if (this.atBottom()) { - this.scrollToBottom(); - } - }, - scrollToBottom: function() { - this.$el.scrollTop(this.el.scrollHeight); - this.measureScrollPosition(); - }, - addOne: function(model) { - var view; - if (model.isExpirationTimerUpdate()) { - view = new Whisper.ExpirationTimerUpdateView({model: model}).render(); - } else if (model.get('type') === 'keychange') { - view = new Whisper.KeyChangeView({model: model}).render(); - } else if (model.get('type') === 'verified-change') { - view = new Whisper.VerifiedChangeView({model: model}).render(); - } else { - view = new this.itemView({model: model}).render(); - this.listenTo(view, 'beforeChangeHeight', this.measureScrollPosition); - this.listenTo(view, 'afterChangeHeight', this.scrollToBottomIfNeeded); - } + this.triggerLazyScroll(); + }, + atBottom: function() { + return this.bottomOffset < 30; + }, + measureScrollPosition: function() { + if (this.el.scrollHeight === 0) { + // hidden + return; + } + this.outerHeight = this.$el.outerHeight(); + this.scrollPosition = this.$el.scrollTop() + this.outerHeight; + this.scrollHeight = this.el.scrollHeight; + this.bottomOffset = this.scrollHeight - this.scrollPosition; + }, + resetScrollPosition: function() { + this.$el.scrollTop(this.scrollPosition - this.$el.outerHeight()); + }, + scrollToBottomIfNeeded: function() { + // This is counter-intuitive. Our current bottomOffset is reflective of what + // we last measured, not necessarily the current state. And this is called + // after we just made a change to the DOM: inserting a message, or an image + // finished loading. So if we were near the bottom before, we _need_ to be + // at the bottom again. So we scroll to the bottom. + if (this.atBottom()) { + this.scrollToBottom(); + } + }, + scrollToBottom: function() { + this.$el.scrollTop(this.el.scrollHeight); + this.measureScrollPosition(); + }, + addOne: function(model) { + var view; + if (model.isExpirationTimerUpdate()) { + view = new Whisper.ExpirationTimerUpdateView({ model: model }).render(); + } else if (model.get('type') === 'keychange') { + view = new Whisper.KeyChangeView({ model: model }).render(); + } else if (model.get('type') === 'verified-change') { + view = new Whisper.VerifiedChangeView({ model: model }).render(); + } else { + view = new this.itemView({ model: model }).render(); + this.listenTo(view, 'beforeChangeHeight', this.measureScrollPosition); + this.listenTo(view, 'afterChangeHeight', this.scrollToBottomIfNeeded); + } - var index = this.collection.indexOf(model); - this.measureScrollPosition(); + var index = this.collection.indexOf(model); + this.measureScrollPosition(); - if (model.get('unread') && !this.atBottom()) { - this.$el.trigger('newOffscreenMessage'); - } + if (model.get('unread') && !this.atBottom()) { + this.$el.trigger('newOffscreenMessage'); + } - if (index === this.collection.length - 1) { - // add to the bottom. - this.$el.append(view.el); - } else if (index === 0) { - // add to top - this.$el.prepend(view.el); - } else { - // insert - var next = this.$('#' + this.collection.at(index + 1).id); - var prev = this.$('#' + this.collection.at(index - 1).id); - if (next.length > 0) { - view.$el.insertBefore(next); - } else if (prev.length > 0) { - view.$el.insertAfter(prev); - } else { - // scan for the right spot - var elements = this.$el.children(); - if (elements.length > 0) { - for (var i = 0; i < elements.length; ++i) { - var m = this.collection.get(elements[i].id); - var m_index = this.collection.indexOf(m); - if (m_index > index) { - view.$el.insertBefore(elements[i]); - break; - } - } - } else { - this.$el.append(view.el); - } - } + if (index === this.collection.length - 1) { + // add to the bottom. + this.$el.append(view.el); + } else if (index === 0) { + // add to top + this.$el.prepend(view.el); + } else { + // insert + var next = this.$('#' + this.collection.at(index + 1).id); + var prev = this.$('#' + this.collection.at(index - 1).id); + if (next.length > 0) { + view.$el.insertBefore(next); + } else if (prev.length > 0) { + view.$el.insertAfter(prev); + } else { + // scan for the right spot + var elements = this.$el.children(); + if (elements.length > 0) { + for (var i = 0; i < elements.length; ++i) { + var m = this.collection.get(elements[i].id); + var m_index = this.collection.indexOf(m); + if (m_index > index) { + view.$el.insertBefore(elements[i]); + break; + } } - this.scrollToBottomIfNeeded(); - }, - }); + } else { + this.$el.append(view.el); + } + } + } + this.scrollToBottomIfNeeded(); + }, + }); })(); diff --git a/js/views/message_view.js b/js/views/message_view.js index e20a2f92d..0cd3e06ae 100644 --- a/js/views/message_view.js +++ b/js/views/message_view.js @@ -7,7 +7,7 @@ /* global $: false */ // eslint-disable-next-line func-names -(function () { +(function() { 'use strict'; const { Signal } = window; @@ -71,7 +71,10 @@ const elapsed = (totalTime - remainingTime) / totalTime; this.$('.sand').css('transform', `translateY(${elapsed * 100}%)`); this.$el.css('display', 'inline-block'); - this.timeout = setTimeout(this.update.bind(this), Math.max(totalTime / 100, 500)); + this.timeout = setTimeout( + this.update.bind(this), + Math.max(totalTime / 100, 500) + ); } return this; }, @@ -195,9 +198,17 @@ this.listenTo(this.model, 'change:body', this.render); this.listenTo(this.model, 'change:delivered', this.renderDelivered); this.listenTo(this.model, 'change:read_by', this.renderRead); - this.listenTo(this.model, 'change:expirationStartTimestamp', this.renderExpiring); + this.listenTo( + this.model, + 'change:expirationStartTimestamp', + this.renderExpiring + ); this.listenTo(this.model, 'change', this.onChange); - this.listenTo(this.model, 'change:flags change:group_update', this.renderControl); + this.listenTo( + this.model, + 'change:flags change:group_update', + this.renderControl + ); this.listenTo(this.model, 'destroy', this.onDestroy); this.listenTo(this.model, 'unload', this.onUnload); this.listenTo(this.model, 'expired', this.onExpired); @@ -225,7 +236,7 @@ this.model.get('errors'), this.model.isReplayableError.bind(this.model) ); - _.map(retrys, 'number').forEach((number) => { + _.map(retrys, 'number').forEach(number => { this.model.resend(number); }); }, @@ -251,7 +262,7 @@ }, onExpired() { this.$el.addClass('expired'); - this.$el.find('.bubble').one('webkitAnimationEnd animationend', (e) => { + this.$el.find('.bubble').one('webkitAnimationEnd animationend', e => { if (e.target === this.$('.bubble')[0]) { this.remove(); } @@ -284,8 +295,9 @@ // as our tests rely on `onUnload` synchronously removing the view from // the DOM. // eslint-disable-next-line more/no-then - this.loadAttachmentViews() - .then(views => views.forEach(view => view.unload())); + this.loadAttachmentViews().then(views => + views.forEach(view => view.unload()) + ); // No need to handle this one, since it listens to 'unload' itself: // this.timerView @@ -321,7 +333,9 @@ } }, renderDelivered() { - if (this.model.get('delivered')) { this.$el.addClass('delivered'); } + if (this.model.get('delivered')) { + this.$el.addClass('delivered'); + } }, renderRead() { if (!_.isEmpty(this.model.get('read_by'))) { @@ -345,7 +359,9 @@ } if (_.size(errors) > 0) { if (this.model.isIncoming()) { - this.$('.content').text(this.model.getDescription()).addClass('error-message'); + this.$('.content') + .text(this.model.getDescription()) + .addClass('error-message'); } this.errorIconView = new ErrorIconView({ model: errors[0] }); this.errorIconView.render().$el.appendTo(this.$('.bubble')); @@ -354,7 +370,9 @@ if (!el || el.length === 0) { this.$('.inner-bubble').append("
"); } - this.$('.content').text(i18n('noContents')).addClass('error-message'); + this.$('.content') + .text(i18n('noContents')) + .addClass('error-message'); } this.$('.meta .hasRetry').remove(); @@ -461,18 +479,24 @@ const hasAttachments = attachments && attachments.length > 0; const hasBody = this.hasTextContents(); - this.$el.html(Mustache.render(_.result(this, 'template', ''), { - message: this.model.get('body'), - hasBody, - timestamp: this.model.get('sent_at'), - sender: (contact && contact.getTitle()) || '', - avatar: (contact && contact.getAvatar()), - profileName: (contact && contact.getProfileName()), - innerBubbleClasses: this.isImageWithoutCaption() ? '' : 'with-tail', - hoverIcon: !hasErrors, - hasAttachments, - reply: i18n('replyToMessage'), - }, this.render_partials())); + this.$el.html( + Mustache.render( + _.result(this, 'template', ''), + { + message: this.model.get('body'), + hasBody, + timestamp: this.model.get('sent_at'), + sender: (contact && contact.getTitle()) || '', + avatar: contact && contact.getAvatar(), + profileName: contact && contact.getProfileName(), + innerBubbleClasses: this.isImageWithoutCaption() ? '' : 'with-tail', + hoverIcon: !hasErrors, + hasAttachments, + reply: i18n('replyToMessage'), + }, + this.render_partials() + ) + ); this.timeStampView.setElement(this.$('.timestamp')); this.timeStampView.update(); @@ -498,7 +522,9 @@ // as our code / Backbone seems to rely on `render` synchronously returning // `this` instead of `Promise MessageView` (this): // eslint-disable-next-line more/no-then - this.loadAttachmentViews().then(views => this.renderAttachmentViews(views)); + this.loadAttachmentViews().then(views => + this.renderAttachmentViews(views) + ); return this; }, @@ -523,22 +549,26 @@ } const attachments = this.model.get('attachments') || []; - const loadedAttachmentViews = Promise.all(attachments.map(attachment => - new Promise(async (resolve) => { - const attachmentWithData = await loadAttachmentData(attachment); - const view = new Whisper.AttachmentView({ - model: attachmentWithData, - timestamp: this.model.get('sent_at'), - }); + const loadedAttachmentViews = Promise.all( + attachments.map( + attachment => + new Promise(async resolve => { + const attachmentWithData = await loadAttachmentData(attachment); + const view = new Whisper.AttachmentView({ + model: attachmentWithData, + timestamp: this.model.get('sent_at'), + }); - this.listenTo(view, 'update', () => { - // NOTE: Can we do without `updated` flag now that we use promises? - view.updated = true; - resolve(view); - }); + this.listenTo(view, 'update', () => { + // NOTE: Can we do without `updated` flag now that we use promises? + view.updated = true; + resolve(view); + }); - view.render(); - }))); + view.render(); + }) + ) + ); // Memoize attachment views to avoid double loading: this.loadedAttachmentViews = loadedAttachmentViews; @@ -550,8 +580,10 @@ }, renderAttachmentView(view) { if (!view.updated) { - throw new Error('Invariant violation:' + - ' Cannot render an attachment view that isn’t ready'); + throw new Error( + 'Invariant violation:' + + ' Cannot render an attachment view that isn’t ready' + ); } const parent = this.$('.attachments')[0]; @@ -570,4 +602,4 @@ this.trigger('afterChangeHeight'); }, }); -}()); +})(); diff --git a/js/views/network_status_view.js b/js/views/network_status_view.js index a66a23da9..9c612811d 100644 --- a/js/views/network_status_view.js +++ b/js/views/network_status_view.js @@ -1,114 +1,120 @@ -(function () { - 'use strict'; +(function() { + 'use strict'; - window.Whisper = window.Whisper || {}; + window.Whisper = window.Whisper || {}; - Whisper.NetworkStatusView = Whisper.View.extend({ - className: 'network-status', - templateName: 'networkStatus', - initialize: function() { - this.$el.hide(); + Whisper.NetworkStatusView = Whisper.View.extend({ + className: 'network-status', + templateName: 'networkStatus', + initialize: function() { + this.$el.hide(); - this.renderIntervalHandle = setInterval(this.update.bind(this), 5000); - extension.windows.onClosed(function () { - clearInterval(this.renderIntervalHandle); - }.bind(this)); + this.renderIntervalHandle = setInterval(this.update.bind(this), 5000); + extension.windows.onClosed( + function() { + clearInterval(this.renderIntervalHandle); + }.bind(this) + ); - setTimeout(this.finishConnectingGracePeriod.bind(this), 5000); + setTimeout(this.finishConnectingGracePeriod.bind(this), 5000); - this.withinConnectingGracePeriod = true; - this.setSocketReconnectInterval(null); + this.withinConnectingGracePeriod = true; + this.setSocketReconnectInterval(null); - window.addEventListener('online', this.update.bind(this)); - window.addEventListener('offline', this.update.bind(this)); + window.addEventListener('online', this.update.bind(this)); + window.addEventListener('offline', this.update.bind(this)); - this.model = new Backbone.Model(); - this.listenTo(this.model, 'change', this.onChange); - }, - onReconnectTimer: function() { - this.setSocketReconnectInterval(60000); - }, - finishConnectingGracePeriod: function() { - this.withinConnectingGracePeriod = false; - }, - setSocketReconnectInterval: function(millis) { - this.socketReconnectWaitDuration = moment.duration(millis); - }, - navigatorOnLine: function() { return navigator.onLine; }, - getSocketStatus: function() { return window.getSocketStatus(); }, - getNetworkStatus: function() { - - var message = ''; - var instructions = ''; - var hasInterruption = false; - var action = null; - var buttonClass = null; - - var socketStatus = this.getSocketStatus(); - switch(socketStatus) { - case WebSocket.CONNECTING: - message = i18n('connecting'); - this.setSocketReconnectInterval(null); - break; - case WebSocket.OPEN: - this.setSocketReconnectInterval(null); - break; - case WebSocket.CLOSING: - message = i18n('disconnected'); - instructions = i18n('checkNetworkConnection'); - hasInterruption = true; - break; - case WebSocket.CLOSED: - message = i18n('disconnected'); - instructions = i18n('checkNetworkConnection'); - hasInterruption = true; - break; - } - - if (socketStatus == WebSocket.CONNECTING && !this.withinConnectingGracePeriod) { - hasInterruption = true; - } - if (this.socketReconnectWaitDuration.asSeconds() > 0) { - instructions = i18n('attemptingReconnection', [this.socketReconnectWaitDuration.asSeconds()]); - } - if (!this.navigatorOnLine()) { - hasInterruption = true; - message = i18n('offline'); - instructions = i18n('checkNetworkConnection'); - } else if (!Whisper.Registration.isDone()) { - hasInterruption = true; - message = i18n('Unlinked'); - instructions = i18n('unlinkedWarning'); - action = i18n('relink'); - buttonClass = 'openInstaller'; - } - - return { - message: message, - instructions: instructions, - hasInterruption: hasInterruption, - action: action, - buttonClass: buttonClass - }; - }, - update: function() { - var status = this.getNetworkStatus(); - this.model.set(status); - }, - render_attributes: function() { - return this.model.attributes; - }, - onChange: function() { - this.render(); - if (this.model.attributes.hasInterruption) { - this.$el.slideDown(); - } - else { - this.$el.hide(); - } - } - }); + this.model = new Backbone.Model(); + this.listenTo(this.model, 'change', this.onChange); + }, + onReconnectTimer: function() { + this.setSocketReconnectInterval(60000); + }, + finishConnectingGracePeriod: function() { + this.withinConnectingGracePeriod = false; + }, + setSocketReconnectInterval: function(millis) { + this.socketReconnectWaitDuration = moment.duration(millis); + }, + navigatorOnLine: function() { + return navigator.onLine; + }, + getSocketStatus: function() { + return window.getSocketStatus(); + }, + getNetworkStatus: function() { + var message = ''; + var instructions = ''; + var hasInterruption = false; + var action = null; + var buttonClass = null; + var socketStatus = this.getSocketStatus(); + switch (socketStatus) { + case WebSocket.CONNECTING: + message = i18n('connecting'); + this.setSocketReconnectInterval(null); + break; + case WebSocket.OPEN: + this.setSocketReconnectInterval(null); + break; + case WebSocket.CLOSING: + message = i18n('disconnected'); + instructions = i18n('checkNetworkConnection'); + hasInterruption = true; + break; + case WebSocket.CLOSED: + message = i18n('disconnected'); + instructions = i18n('checkNetworkConnection'); + hasInterruption = true; + break; + } + if ( + socketStatus == WebSocket.CONNECTING && + !this.withinConnectingGracePeriod + ) { + hasInterruption = true; + } + if (this.socketReconnectWaitDuration.asSeconds() > 0) { + instructions = i18n('attemptingReconnection', [ + this.socketReconnectWaitDuration.asSeconds(), + ]); + } + if (!this.navigatorOnLine()) { + hasInterruption = true; + message = i18n('offline'); + instructions = i18n('checkNetworkConnection'); + } else if (!Whisper.Registration.isDone()) { + hasInterruption = true; + message = i18n('Unlinked'); + instructions = i18n('unlinkedWarning'); + action = i18n('relink'); + buttonClass = 'openInstaller'; + } + return { + message: message, + instructions: instructions, + hasInterruption: hasInterruption, + action: action, + buttonClass: buttonClass, + }; + }, + update: function() { + var status = this.getNetworkStatus(); + this.model.set(status); + }, + render_attributes: function() { + return this.model.attributes; + }, + onChange: function() { + this.render(); + if (this.model.attributes.hasInterruption) { + this.$el.slideDown(); + } else { + this.$el.hide(); + } + }, + }); })(); diff --git a/js/views/new_group_update_view.js b/js/views/new_group_update_view.js index dbd88c4a7..9edce4405 100644 --- a/js/views/new_group_update_view.js +++ b/js/views/new_group_update_view.js @@ -1,82 +1,89 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.NewGroupUpdateView = Whisper.View.extend({ - tagName: "div", - className: 'new-group-update', - templateName: 'new-group-update', - initialize: function(options) { - this.render(); - this.avatarInput = new Whisper.FileInputView({ - el: this.$('.group-avatar'), - window: options.window - }); + Whisper.NewGroupUpdateView = Whisper.View.extend({ + tagName: 'div', + className: 'new-group-update', + templateName: 'new-group-update', + initialize: function(options) { + this.render(); + this.avatarInput = new Whisper.FileInputView({ + el: this.$('.group-avatar'), + window: options.window, + }); - this.recipients_view = new Whisper.RecipientsInputView(); - this.listenTo(this.recipients_view.typeahead, 'sync', function() { - this.model.contactCollection.models.forEach(function(model) { - if (this.recipients_view.typeahead.get(model)) { - this.recipients_view.typeahead.remove(model); - } - }.bind(this)); - }); - this.recipients_view.$el.insertBefore(this.$('.container')); + this.recipients_view = new Whisper.RecipientsInputView(); + this.listenTo(this.recipients_view.typeahead, 'sync', function() { + this.model.contactCollection.models.forEach( + function(model) { + if (this.recipients_view.typeahead.get(model)) { + this.recipients_view.typeahead.remove(model); + } + }.bind(this) + ); + }); + this.recipients_view.$el.insertBefore(this.$('.container')); - this.member_list_view = new Whisper.ContactListView({ - collection: this.model.contactCollection, - className: 'members' - }); - this.member_list_view.render(); - this.$('.scrollable').append(this.member_list_view.el); - }, - events: { - 'click .back': 'goBack', - 'click .send': 'send', - 'focusin input.search': 'showResults', - 'focusout input.search': 'hideResults', - }, - hideResults: function() { - this.$('.results').hide(); - }, - showResults: function() { - this.$('.results').show(); - }, - goBack: function() { - this.trigger('back'); - }, - render_attributes: function() { - return { - name: this.model.getTitle(), - avatar: this.model.getAvatar() - }; - }, - send: function() { - return this.avatarInput.getThumbnail().then(function(avatarFile) { - var now = Date.now(); - var attrs = { - timestamp: now, - active_at: now, - name: this.$('.name').val(), - members: _.union(this.model.get('members'), this.recipients_view.recipients.pluck('id')) - }; - if (avatarFile) { - attrs.avatar = avatarFile; - } - this.model.set(attrs); - var group_update = this.model.changed; - this.model.save(); + this.member_list_view = new Whisper.ContactListView({ + collection: this.model.contactCollection, + className: 'members', + }); + this.member_list_view.render(); + this.$('.scrollable').append(this.member_list_view.el); + }, + events: { + 'click .back': 'goBack', + 'click .send': 'send', + 'focusin input.search': 'showResults', + 'focusout input.search': 'hideResults', + }, + hideResults: function() { + this.$('.results').hide(); + }, + showResults: function() { + this.$('.results').show(); + }, + goBack: function() { + this.trigger('back'); + }, + render_attributes: function() { + return { + name: this.model.getTitle(), + avatar: this.model.getAvatar(), + }; + }, + send: function() { + return this.avatarInput.getThumbnail().then( + function(avatarFile) { + var now = Date.now(); + var attrs = { + timestamp: now, + active_at: now, + name: this.$('.name').val(), + members: _.union( + this.model.get('members'), + this.recipients_view.recipients.pluck('id') + ), + }; + if (avatarFile) { + attrs.avatar = avatarFile; + } + this.model.set(attrs); + var group_update = this.model.changed; + this.model.save(); - if (group_update.avatar) { - this.model.trigger('change:avatar'); - } + if (group_update.avatar) { + this.model.trigger('change:avatar'); + } - this.model.updateGroup(group_update); - this.goBack(); - }.bind(this)); - } - }); + this.model.updateGroup(group_update); + this.goBack(); + }.bind(this) + ); + }, + }); })(); diff --git a/js/views/phone-input-view.js b/js/views/phone-input-view.js index a43c0e4b7..7b69e1ded 100644 --- a/js/views/phone-input-view.js +++ b/js/views/phone-input-view.js @@ -1,36 +1,38 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.PhoneInputView = Whisper.View.extend({ - tagName: 'div', - className: 'phone-input', - templateName: 'phone-number', - initialize: function() { - this.$('input.number').intlTelInput(); - }, - events: { - 'change': 'validateNumber', - 'keyup': 'validateNumber' - }, - validateNumber: function() { - var input = this.$('input.number'); - var regionCode = this.$('li.active').attr('data-country-code').toUpperCase(); - var number = input.val(); + Whisper.PhoneInputView = Whisper.View.extend({ + tagName: 'div', + className: 'phone-input', + templateName: 'phone-number', + initialize: function() { + this.$('input.number').intlTelInput(); + }, + events: { + change: 'validateNumber', + keyup: 'validateNumber', + }, + validateNumber: function() { + var input = this.$('input.number'); + var regionCode = this.$('li.active') + .attr('data-country-code') + .toUpperCase(); + var number = input.val(); - var parsedNumber = libphonenumber.util.parseNumber(number, regionCode); - if (parsedNumber.isValidNumber) { - this.$('.number-container').removeClass('invalid'); - this.$('.number-container').addClass('valid'); - } else { - this.$('.number-container').removeClass('valid'); - } - input.trigger('validation'); + var parsedNumber = libphonenumber.util.parseNumber(number, regionCode); + if (parsedNumber.isValidNumber) { + this.$('.number-container').removeClass('invalid'); + this.$('.number-container').addClass('valid'); + } else { + this.$('.number-container').removeClass('valid'); + } + input.trigger('validation'); - return parsedNumber.e164; - } - }); + return parsedNumber.e164; + }, + }); })(); diff --git a/js/views/react_wrapper_view.js b/js/views/react_wrapper_view.js index 501e654b0..ae372698c 100644 --- a/js/views/react_wrapper_view.js +++ b/js/views/react_wrapper_view.js @@ -4,7 +4,7 @@ /* global ReactDOM: false */ // eslint-disable-next-line func-names -(function () { +(function() { 'use strict'; window.Whisper = window.Whisper || {}; @@ -44,4 +44,4 @@ Backbone.View.prototype.remove.call(this); }, }); -}()); +})(); diff --git a/js/views/recipients_input_view.js b/js/views/recipients_input_view.js index d42359723..2e72b9054 100644 --- a/js/views/recipients_input_view.js +++ b/js/views/recipients_input_view.js @@ -1,185 +1,182 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - var ContactsTypeahead = Backbone.TypeaheadCollection.extend({ - typeaheadAttributes: [ - 'name', - 'e164_number', - 'national_number', - 'international_number' - ], - database: Whisper.Database, - storeName: 'conversations', - model: Whisper.Conversation, - fetchContacts: function() { - return this.fetch({ reset: true, conditions: { type: 'private' } }); + var ContactsTypeahead = Backbone.TypeaheadCollection.extend({ + typeaheadAttributes: [ + 'name', + 'e164_number', + 'national_number', + 'international_number', + ], + database: Whisper.Database, + storeName: 'conversations', + model: Whisper.Conversation, + fetchContacts: function() { + return this.fetch({ reset: true, conditions: { type: 'private' } }); + }, + }); + + Whisper.ContactPillView = Whisper.View.extend({ + tagName: 'span', + className: 'recipient', + events: { + 'click .remove': 'removeModel', + }, + templateName: 'contact_pill', + initialize: function() { + var error = this.model.validate(this.model.attributes); + if (error) { + this.$el.addClass('error'); + } + }, + removeModel: function() { + this.$el.trigger('remove', { modelId: this.model.id }); + this.remove(); + }, + render_attributes: function() { + return { name: this.model.getTitle() }; + }, + }); + + Whisper.RecipientListView = Whisper.ListView.extend({ + itemView: Whisper.ContactPillView, + }); + + Whisper.SuggestionView = Whisper.ConversationListItemView.extend({ + className: 'contact-details contact', + templateName: 'contact_name_and_number', + }); + + Whisper.SuggestionListView = Whisper.ConversationListView.extend({ + itemView: Whisper.SuggestionView, + }); + + Whisper.RecipientsInputView = Whisper.View.extend({ + className: 'recipients-input', + templateName: 'recipients-input', + initialize: function(options) { + if (options) { + this.placeholder = options.placeholder; + } + this.render(); + this.$input = this.$('input.search'); + this.$new_contact = this.$('.new-contact'); + + // Collection of recipients selected for the new message + this.recipients = new Whisper.ConversationCollection([], { + comparator: false, + }); + + // View to display the selected recipients + this.recipients_view = new Whisper.RecipientListView({ + collection: this.recipients, + el: this.$('.recipients'), + }); + + // Collection of contacts to match user input against + this.typeahead = new ContactsTypeahead(); + this.typeahead.fetchContacts(); + + // View to display the matched contacts from typeahead + this.typeahead_view = new Whisper.SuggestionListView({ + collection: new Whisper.ConversationCollection([], { + comparator: function(m) { + return m.getTitle().toLowerCase(); + }, + }), + }); + this.$('.contacts').append(this.typeahead_view.el); + this.initNewContact(); + this.listenTo(this.typeahead, 'reset', this.filterContacts); + }, + + render_attributes: function() { + return { placeholder: this.placeholder || 'name or phone number' }; + }, + + events: { + 'input input.search': 'filterContacts', + 'select .new-contact': 'addNewRecipient', + 'select .contacts': 'addRecipient', + 'remove .recipient': 'removeRecipient', + }, + + filterContacts: function(e) { + var query = this.$input.val(); + if (query.length) { + if (this.maybeNumber(query)) { + this.new_contact_view.model.set('id', query); + this.new_contact_view.render().$el.show(); + } else { + this.new_contact_view.$el.hide(); } - }); + this.typeahead_view.collection.reset(this.typeahead.typeahead(query)); + } else { + this.resetTypeahead(); + } + }, - Whisper.ContactPillView = Whisper.View.extend({ - tagName: 'span', - className: 'recipient', - events: { - 'click .remove': 'removeModel' - }, - templateName: 'contact_pill', - initialize: function() { - var error = this.model.validate(this.model.attributes); - if (error) { - this.$el.addClass('error'); - } - }, - removeModel: function() { - this.$el.trigger('remove', {modelId: this.model.id}); - this.remove(); - }, - render_attributes: function() { - return { name: this.model.getTitle() }; - } - }); + initNewContact: function() { + if (this.new_contact_view) { + this.new_contact_view.undelegateEvents(); + this.new_contact_view.$el.hide(); + } + // Creates a view to display a new contact + this.new_contact_view = new Whisper.ConversationListItemView({ + el: this.$new_contact, + model: ConversationController.create({ + type: 'private', + newContact: true, + }), + }).render(); + }, - Whisper.RecipientListView = Whisper.ListView.extend({ - itemView: Whisper.ContactPillView - }); + addNewRecipient: function() { + this.recipients.add(this.new_contact_view.model); + this.initNewContact(); + this.resetTypeahead(); + }, - Whisper.SuggestionView = Whisper.ConversationListItemView.extend({ - className: 'contact-details contact', - templateName: 'contact_name_and_number', - }); + addRecipient: function(e, conversation) { + this.recipients.add(this.typeahead.remove(conversation.id)); + this.resetTypeahead(); + }, - Whisper.SuggestionListView = Whisper.ConversationListView.extend({ - itemView: Whisper.SuggestionView - }); + removeRecipient: function(e, data) { + var model = this.recipients.remove(data.modelId); + if (!model.get('newContact')) { + this.typeahead.add(model); + } + this.filterContacts(); + }, - Whisper.RecipientsInputView = Whisper.View.extend({ - className: 'recipients-input', - templateName: 'recipients-input', - initialize: function(options) { - if (options) { - this.placeholder = options.placeholder; - } - this.render(); - this.$input = this.$('input.search'); - this.$new_contact = this.$('.new-contact'); + reset: function() { + this.delegateEvents(); + this.typeahead_view.delegateEvents(); + this.recipients_view.delegateEvents(); + this.new_contact_view.delegateEvents(); + this.typeahead.add( + this.recipients.filter(function(model) { + return !model.get('newContact'); + }) + ); + this.recipients.reset([]); + this.resetTypeahead(); + this.typeahead.fetchContacts(); + }, - // Collection of recipients selected for the new message - this.recipients = new Whisper.ConversationCollection([], { - comparator: false - }); - - // View to display the selected recipients - this.recipients_view = new Whisper.RecipientListView({ - collection: this.recipients, - el: this.$('.recipients') - }); - - // Collection of contacts to match user input against - this.typeahead = new ContactsTypeahead(); - this.typeahead.fetchContacts(); - - // View to display the matched contacts from typeahead - this.typeahead_view = new Whisper.SuggestionListView({ - collection : new Whisper.ConversationCollection([], { - comparator: function(m) { return m.getTitle().toLowerCase(); } - }) - }); - this.$('.contacts').append(this.typeahead_view.el); - this.initNewContact(); - this.listenTo(this.typeahead, 'reset', this.filterContacts); - - }, - - render_attributes: function() { - return { placeholder: this.placeholder || "name or phone number" }; - }, - - events: { - 'input input.search': 'filterContacts', - 'select .new-contact': 'addNewRecipient', - 'select .contacts': 'addRecipient', - 'remove .recipient': 'removeRecipient', - }, - - filterContacts: function(e) { - var query = this.$input.val(); - if (query.length) { - if (this.maybeNumber(query)) { - this.new_contact_view.model.set('id', query); - this.new_contact_view.render().$el.show(); - } else { - this.new_contact_view.$el.hide(); - } - this.typeahead_view.collection.reset( - this.typeahead.typeahead(query) - ); - } else { - this.resetTypeahead(); - } - }, - - initNewContact: function() { - if (this.new_contact_view) { - this.new_contact_view.undelegateEvents(); - this.new_contact_view.$el.hide(); - } - // Creates a view to display a new contact - this.new_contact_view = new Whisper.ConversationListItemView({ - el: this.$new_contact, - model: ConversationController.create({ - type: 'private', - newContact: true - }) - }).render(); - }, - - addNewRecipient: function() { - this.recipients.add(this.new_contact_view.model); - this.initNewContact(); - this.resetTypeahead(); - }, - - addRecipient: function(e, conversation) { - this.recipients.add(this.typeahead.remove(conversation.id)); - this.resetTypeahead(); - }, - - removeRecipient: function(e, data) { - var model = this.recipients.remove(data.modelId); - if (!model.get('newContact')) { - this.typeahead.add(model); - } - this.filterContacts(); - }, - - reset: function() { - this.delegateEvents(); - this.typeahead_view.delegateEvents(); - this.recipients_view.delegateEvents(); - this.new_contact_view.delegateEvents(); - this.typeahead.add( - this.recipients.filter(function(model) { - return !model.get('newContact'); - }) - ); - this.recipients.reset([]); - this.resetTypeahead(); - this.typeahead.fetchContacts(); - }, - - resetTypeahead: function() { - this.new_contact_view.$el.hide(); - this.$input.val('').focus(); - this.typeahead_view.collection.reset([]); - }, - - - maybeNumber: function(number) { - return number.match(/^\+?[0-9]*$/); - } - }); + resetTypeahead: function() { + this.new_contact_view.$el.hide(); + this.$input.val('').focus(); + this.typeahead_view.collection.reset([]); + }, + maybeNumber: function(number) { + return number.match(/^\+?[0-9]*$/); + }, + }); })(); diff --git a/js/views/recorder_view.js b/js/views/recorder_view.js index b8ee2e114..17e036fdd 100644 --- a/js/views/recorder_view.js +++ b/js/views/recorder_view.js @@ -1,80 +1,84 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.RecorderView = Whisper.View.extend({ - className: 'recorder clearfix', - templateName: 'recorder', - initialize: function() { - this.startTime = Date.now(); - this.interval = setInterval(this.updateTime.bind(this), 1000); - this.start(); - }, - events: { - 'click .close': 'close', - 'click .finish': 'finish', - 'close': 'close' - }, - updateTime: function() { - var duration = moment.duration(Date.now() - this.startTime, 'ms'); - var minutes = '' + Math.trunc(duration.asMinutes()); - var seconds = '' + duration.seconds(); - if (seconds.length < 2) { - seconds = '0' + seconds; - } - this.$('.time').text(minutes + ':' + seconds); - }, - close: function() { - // Note: the 'close' event can be triggered by InboxView, when the user clicks - // anywhere outside the recording pane. + Whisper.RecorderView = Whisper.View.extend({ + className: 'recorder clearfix', + templateName: 'recorder', + initialize: function() { + this.startTime = Date.now(); + this.interval = setInterval(this.updateTime.bind(this), 1000); + this.start(); + }, + events: { + 'click .close': 'close', + 'click .finish': 'finish', + close: 'close', + }, + updateTime: function() { + var duration = moment.duration(Date.now() - this.startTime, 'ms'); + var minutes = '' + Math.trunc(duration.asMinutes()); + var seconds = '' + duration.seconds(); + if (seconds.length < 2) { + seconds = '0' + seconds; + } + this.$('.time').text(minutes + ':' + seconds); + }, + close: function() { + // Note: the 'close' event can be triggered by InboxView, when the user clicks + // anywhere outside the recording pane. - if (this.recorder.isRecording()) { - this.recorder.cancelRecording(); - } - if (this.interval) { - clearInterval(this.interval); - } - if (this.source) { - this.source.disconnect(); - } - if (this.context) { - this.context.close().then(function() { - console.log('audio context closed'); - }); - } - this.remove(); - this.trigger('closed'); - }, - finish: function() { - this.recorder.finishRecording(); - this.close(); - }, - handleBlob: function(recorder, blob) { - if (blob) { - this.trigger('send', blob); - } - }, - start: function() { - this.context = new AudioContext(); - this.input = this.context.createGain(); - this.recorder = new WebAudioRecorder(this.input, { - encoding: 'mp3', - workerDir: 'js/' // must end with slash - }); - this.recorder.onComplete = this.handleBlob.bind(this); - this.recorder.onError = this.onError; - navigator.webkitGetUserMedia({ audio: true }, function(stream) { - this.source = this.context.createMediaStreamSource(stream); - this.source.connect(this.input); - }.bind(this), this.onError.bind(this)); - this.recorder.startRecording(); - }, - onError: function(error) { - console.log(error.stack); - this.close(); - } - }); + if (this.recorder.isRecording()) { + this.recorder.cancelRecording(); + } + if (this.interval) { + clearInterval(this.interval); + } + if (this.source) { + this.source.disconnect(); + } + if (this.context) { + this.context.close().then(function() { + console.log('audio context closed'); + }); + } + this.remove(); + this.trigger('closed'); + }, + finish: function() { + this.recorder.finishRecording(); + this.close(); + }, + handleBlob: function(recorder, blob) { + if (blob) { + this.trigger('send', blob); + } + }, + start: function() { + this.context = new AudioContext(); + this.input = this.context.createGain(); + this.recorder = new WebAudioRecorder(this.input, { + encoding: 'mp3', + workerDir: 'js/', // must end with slash + }); + this.recorder.onComplete = this.handleBlob.bind(this); + this.recorder.onError = this.onError; + navigator.webkitGetUserMedia( + { audio: true }, + function(stream) { + this.source = this.context.createMediaStreamSource(stream); + this.source.connect(this.input); + }.bind(this), + this.onError.bind(this) + ); + this.recorder.startRecording(); + }, + onError: function(error) { + console.log(error.stack); + this.close(); + }, + }); })(); diff --git a/js/views/scroll_down_button_view.js b/js/views/scroll_down_button_view.js index 1b79b4032..690752bc3 100644 --- a/js/views/scroll_down_button_view.js +++ b/js/views/scroll_down_button_view.js @@ -1,39 +1,39 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.ScrollDownButtonView = Whisper.View.extend({ - className: 'scroll-down-button-view', - templateName: 'scroll-down-button-view', + Whisper.ScrollDownButtonView = Whisper.View.extend({ + className: 'scroll-down-button-view', + templateName: 'scroll-down-button-view', - initialize: function(options) { - options = options || {}; - this.count = options.count || 0; - }, + initialize: function(options) { + options = options || {}; + this.count = options.count || 0; + }, - increment: function(count) { - count = count || 0; - this.count += count; - this.render(); - }, + increment: function(count) { + count = count || 0; + this.count += count; + this.render(); + }, - render_attributes: function() { - var cssClass = this.count > 0 ? 'new-messages' : ''; + render_attributes: function() { + var cssClass = this.count > 0 ? 'new-messages' : ''; - var moreBelow = i18n('scrollDown'); - if (this.count > 1) { - moreBelow = i18n('messagesBelow'); - } else if (this.count === 1) { - moreBelow = i18n('messageBelow'); - } + var moreBelow = i18n('scrollDown'); + if (this.count > 1) { + moreBelow = i18n('messagesBelow'); + } else if (this.count === 1) { + moreBelow = i18n('messageBelow'); + } - return { - cssClass: cssClass, - moreBelow: moreBelow - }; - } - }); + return { + cssClass: cssClass, + moreBelow: moreBelow, + }; + }, + }); })(); diff --git a/js/views/settings_view.js b/js/views/settings_view.js index b29bcb457..a8345d210 100644 --- a/js/views/settings_view.js +++ b/js/views/settings_view.js @@ -5,127 +5,127 @@ /* eslint-disable */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; - const { Database } = window.Whisper; - const { OS, Logs } = window.Signal; - const { Settings } = window.Signal.Types; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; + const { Database } = window.Whisper; + const { OS, Logs } = window.Signal; + const { Settings } = window.Signal.Types; - var CheckboxView = Whisper.View.extend({ - initialize: function(options) { - this.name = options.name; - this.defaultValue = options.defaultValue; - this.event = options.event; - this.populate(); - }, - events: { - 'change': 'change' - }, - change: function(e) { - var value = e.target.checked; - storage.put(this.name, value); - console.log(this.name, 'changed to', value); - if (this.event) { - this.$el.trigger(this.event); - } - }, - populate: function() { - var value = storage.get(this.name, this.defaultValue); - this.$('input').prop('checked', !!value); - }, - }); - var RadioButtonGroupView = Whisper.View.extend({ - initialize: function(options) { - this.name = options.name; - this.defaultValue = options.defaultValue; - this.event = options.event; - this.populate(); - }, - events: { - 'change': 'change' - }, - change: function(e) { - var value = this.$(e.target).val(); - storage.put(this.name, value); - console.log(this.name, 'changed to', value); - if (this.event) { - this.$el.trigger(this.event); - } - }, - populate: function() { - var value = storage.get(this.name, this.defaultValue); - this.$('#' + this.name + '-' + value).attr('checked', 'checked'); - }, - }); - Whisper.SettingsView = Whisper.View.extend({ - className: 'settings modal expand', - templateName: 'settings', - initialize: function() { - this.deviceName = textsecure.storage.user.getDeviceName(); - this.render(); - new RadioButtonGroupView({ - el: this.$('.notification-settings'), - defaultValue: 'message', - name: 'notification-setting' - }); - new RadioButtonGroupView({ - el: this.$('.theme-settings'), - defaultValue: 'android', - name: 'theme-setting', - event: 'change-theme' - }); - if (Settings.isAudioNotificationSupported()) { - new CheckboxView({ - el: this.$('.audio-notification-setting'), - defaultValue: false, - name: 'audio-notification' - }); - } - new CheckboxView({ - el: this.$('.menu-bar-setting'), - defaultValue: false, - name: 'hide-menu-bar', - event: 'change-hide-menu' - }); - if (textsecure.storage.user.getDeviceId() != '1') { - var syncView = new SyncView().render(); - this.$('.sync-setting').append(syncView.el); - } - }, - events: { - 'click .close': 'remove', - 'click .clear-data': 'onClearData', - }, - render_attributes: function() { - return { - deviceNameLabel: i18n('deviceName'), - deviceName: this.deviceName, - theme: i18n('theme'), - notifications: i18n('notifications'), - notificationSettingsDialog: i18n('notificationSettingsDialog'), - settings: i18n('settings'), - disableNotifications: i18n('disableNotifications'), - nameAndMessage: i18n('nameAndMessage'), - noNameOrMessage: i18n('noNameOrMessage'), - nameOnly: i18n('nameOnly'), - audioNotificationDescription: i18n('audioNotificationDescription'), - isAudioNotificationSupported: Settings.isAudioNotificationSupported(), - themeAndroidDark: i18n('themeAndroidDark'), - hideMenuBar: i18n('hideMenuBar'), - clearDataHeader: i18n('clearDataHeader'), - clearDataButton: i18n('clearDataButton'), - clearDataExplanation: i18n('clearDataExplanation'), - }; - }, - onClearData: function() { - var clearDataView = new ClearDataView().render(); - $('body').append(clearDataView.el); - }, - }); + var CheckboxView = Whisper.View.extend({ + initialize: function(options) { + this.name = options.name; + this.defaultValue = options.defaultValue; + this.event = options.event; + this.populate(); + }, + events: { + change: 'change', + }, + change: function(e) { + var value = e.target.checked; + storage.put(this.name, value); + console.log(this.name, 'changed to', value); + if (this.event) { + this.$el.trigger(this.event); + } + }, + populate: function() { + var value = storage.get(this.name, this.defaultValue); + this.$('input').prop('checked', !!value); + }, + }); + var RadioButtonGroupView = Whisper.View.extend({ + initialize: function(options) { + this.name = options.name; + this.defaultValue = options.defaultValue; + this.event = options.event; + this.populate(); + }, + events: { + change: 'change', + }, + change: function(e) { + var value = this.$(e.target).val(); + storage.put(this.name, value); + console.log(this.name, 'changed to', value); + if (this.event) { + this.$el.trigger(this.event); + } + }, + populate: function() { + var value = storage.get(this.name, this.defaultValue); + this.$('#' + this.name + '-' + value).attr('checked', 'checked'); + }, + }); + Whisper.SettingsView = Whisper.View.extend({ + className: 'settings modal expand', + templateName: 'settings', + initialize: function() { + this.deviceName = textsecure.storage.user.getDeviceName(); + this.render(); + new RadioButtonGroupView({ + el: this.$('.notification-settings'), + defaultValue: 'message', + name: 'notification-setting', + }); + new RadioButtonGroupView({ + el: this.$('.theme-settings'), + defaultValue: 'android', + name: 'theme-setting', + event: 'change-theme', + }); + if (Settings.isAudioNotificationSupported()) { + new CheckboxView({ + el: this.$('.audio-notification-setting'), + defaultValue: false, + name: 'audio-notification', + }); + } + new CheckboxView({ + el: this.$('.menu-bar-setting'), + defaultValue: false, + name: 'hide-menu-bar', + event: 'change-hide-menu', + }); + if (textsecure.storage.user.getDeviceId() != '1') { + var syncView = new SyncView().render(); + this.$('.sync-setting').append(syncView.el); + } + }, + events: { + 'click .close': 'remove', + 'click .clear-data': 'onClearData', + }, + render_attributes: function() { + return { + deviceNameLabel: i18n('deviceName'), + deviceName: this.deviceName, + theme: i18n('theme'), + notifications: i18n('notifications'), + notificationSettingsDialog: i18n('notificationSettingsDialog'), + settings: i18n('settings'), + disableNotifications: i18n('disableNotifications'), + nameAndMessage: i18n('nameAndMessage'), + noNameOrMessage: i18n('noNameOrMessage'), + nameOnly: i18n('nameOnly'), + audioNotificationDescription: i18n('audioNotificationDescription'), + isAudioNotificationSupported: Settings.isAudioNotificationSupported(), + themeAndroidDark: i18n('themeAndroidDark'), + hideMenuBar: i18n('hideMenuBar'), + clearDataHeader: i18n('clearDataHeader'), + clearDataButton: i18n('clearDataButton'), + clearDataExplanation: i18n('clearDataExplanation'), + }; + }, + onClearData: function() { + var clearDataView = new ClearDataView().render(); + $('body').append(clearDataView.el); + }, + }); - /* jshint ignore:start */ - /* eslint-enable */ + /* jshint ignore:start */ + /* eslint-enable */ const CLEAR_DATA_STEPS = { CHOICE: 1, @@ -160,10 +160,7 @@ }, async clearAllData() { try { - await Promise.all([ - Logs.deleteAll(), - Database.drop(), - ]); + await Promise.all([Logs.deleteAll(), Database.drop()]); } catch (error) { console.log( 'Something went wrong deleting all data:', @@ -186,61 +183,61 @@ }, }); - /* eslint-disable */ - /* jshint ignore:end */ + /* eslint-disable */ + /* jshint ignore:end */ - var SyncView = Whisper.View.extend({ - templateName: 'syncSettings', - className: 'syncSettings', - events: { - 'click .sync': 'sync' - }, - enable: function() { - this.$('.sync').text(i18n('syncNow')); - this.$('.sync').removeAttr('disabled'); - }, - disable: function() { - this.$('.sync').attr('disabled', 'disabled'); - this.$('.sync').text(i18n('syncing')); - }, - onsuccess: function() { - storage.put('synced_at', Date.now()); - console.log('sync successful'); - this.enable(); - this.render(); - }, - ontimeout: function() { - console.log('sync timed out'); - this.$('.synced_at').hide(); - this.$('.sync_failed').show(); - this.enable(); - }, - sync: function() { - this.$('.sync_failed').hide(); - if (textsecure.storage.user.getDeviceId() != '1') { - this.disable(); - var syncRequest = window.getSyncRequest(); - syncRequest.addEventListener('success', this.onsuccess.bind(this)); - syncRequest.addEventListener('timeout', this.ontimeout.bind(this)); - } else { - console.log("Tried to sync from device 1"); - } - }, - render_attributes: function() { - var attrs = { - sync: i18n('sync'), - syncNow: i18n('syncNow'), - syncExplanation: i18n('syncExplanation'), - syncFailed: i18n('syncFailed') - }; - var date = storage.get('synced_at'); - if (date) { - date = new Date(date); - attrs.lastSynced = i18n('lastSynced'); - attrs.syncDate = date.toLocaleDateString(); - attrs.syncTime = date.toLocaleTimeString(); - } - return attrs; - } - }); + var SyncView = Whisper.View.extend({ + templateName: 'syncSettings', + className: 'syncSettings', + events: { + 'click .sync': 'sync', + }, + enable: function() { + this.$('.sync').text(i18n('syncNow')); + this.$('.sync').removeAttr('disabled'); + }, + disable: function() { + this.$('.sync').attr('disabled', 'disabled'); + this.$('.sync').text(i18n('syncing')); + }, + onsuccess: function() { + storage.put('synced_at', Date.now()); + console.log('sync successful'); + this.enable(); + this.render(); + }, + ontimeout: function() { + console.log('sync timed out'); + this.$('.synced_at').hide(); + this.$('.sync_failed').show(); + this.enable(); + }, + sync: function() { + this.$('.sync_failed').hide(); + if (textsecure.storage.user.getDeviceId() != '1') { + this.disable(); + var syncRequest = window.getSyncRequest(); + syncRequest.addEventListener('success', this.onsuccess.bind(this)); + syncRequest.addEventListener('timeout', this.ontimeout.bind(this)); + } else { + console.log('Tried to sync from device 1'); + } + }, + render_attributes: function() { + var attrs = { + sync: i18n('sync'), + syncNow: i18n('syncNow'), + syncExplanation: i18n('syncExplanation'), + syncFailed: i18n('syncFailed'), + }; + var date = storage.get('synced_at'); + if (date) { + date = new Date(date); + attrs.lastSynced = i18n('lastSynced'); + attrs.syncDate = date.toLocaleDateString(); + attrs.syncTime = date.toLocaleTimeString(); + } + return attrs; + }, + }); })(); diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js index bbc074547..a5787e4bb 100644 --- a/js/views/standalone_registration_view.js +++ b/js/views/standalone_registration_view.js @@ -1,88 +1,111 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.StandaloneRegistrationView = Whisper.View.extend({ - templateName: 'standalone', - className: 'full-screen-flow', - initialize: function() { - this.accountManager = getAccountManager(); + Whisper.StandaloneRegistrationView = Whisper.View.extend({ + templateName: 'standalone', + className: 'full-screen-flow', + initialize: function() { + this.accountManager = getAccountManager(); - this.render(); + this.render(); - var number = textsecure.storage.user.getNumber(); - if (number) { - this.$('input.number').val(number); - } - this.phoneView = new Whisper.PhoneInputView({el: this.$('#phone-number-input')}); - this.$('#error').hide(); - }, - events: { - 'validation input.number': 'onValidation', - 'click #request-voice': 'requestVoice', - 'click #request-sms': 'requestSMSVerification', - 'change #code': 'onChangeCode', - 'click #verifyCode': 'verifyCode', - }, - verifyCode: function(e) { - var number = this.phoneView.validateNumber(); - var verificationCode = $('#code').val().replace(/\D+/g, ''); + var number = textsecure.storage.user.getNumber(); + if (number) { + this.$('input.number').val(number); + } + this.phoneView = new Whisper.PhoneInputView({ + el: this.$('#phone-number-input'), + }); + this.$('#error').hide(); + }, + events: { + 'validation input.number': 'onValidation', + 'click #request-voice': 'requestVoice', + 'click #request-sms': 'requestSMSVerification', + 'change #code': 'onChangeCode', + 'click #verifyCode': 'verifyCode', + }, + verifyCode: function(e) { + var number = this.phoneView.validateNumber(); + var verificationCode = $('#code') + .val() + .replace(/\D+/g, ''); - this.accountManager.registerSingleDevice(number, verificationCode).then(function() { - this.$el.trigger('openInbox'); - }.bind(this)).catch(this.log.bind(this)); - }, - log: function (s) { - console.log(s); - this.$('#status').text(s); - }, - validateCode: function() { - var verificationCode = $('#code').val().replace(/\D/g, ''); - if (verificationCode.length == 6) { - return verificationCode; - } - }, - displayError: function(error) { - this.$('#error').hide().text(error).addClass('in').fadeIn(); - }, - onValidation: function() { - if (this.$('#number-container').hasClass('valid')) { - this.$('#request-sms, #request-voice').removeAttr('disabled'); - } else { - this.$('#request-sms, #request-voice').prop('disabled', 'disabled'); - } - }, - onChangeCode: function() { - if (!this.validateCode()) { - this.$('#code').addClass('invalid'); - } else { - this.$('#code').removeClass('invalid'); - } - }, - requestVoice: function() { - window.removeSetupMenuItems(); - this.$('#error').hide(); - var number = this.phoneView.validateNumber(); - if (number) { - this.accountManager.requestVoiceVerification(number).catch(this.displayError.bind(this)); - this.$('#step2').addClass('in').fadeIn(); - } else { - this.$('#number-container').addClass('invalid'); - } - }, - requestSMSVerification: function() { - window.removeSetupMenuItems(); - $('#error').hide(); - var number = this.phoneView.validateNumber(); - if (number) { - this.accountManager.requestSMSVerification(number).catch(this.displayError.bind(this)); - this.$('#step2').addClass('in').fadeIn(); - } else { - this.$('#number-container').addClass('invalid'); - } - } - }); + this.accountManager + .registerSingleDevice(number, verificationCode) + .then( + function() { + this.$el.trigger('openInbox'); + }.bind(this) + ) + .catch(this.log.bind(this)); + }, + log: function(s) { + console.log(s); + this.$('#status').text(s); + }, + validateCode: function() { + var verificationCode = $('#code') + .val() + .replace(/\D/g, ''); + if (verificationCode.length == 6) { + return verificationCode; + } + }, + displayError: function(error) { + this.$('#error') + .hide() + .text(error) + .addClass('in') + .fadeIn(); + }, + onValidation: function() { + if (this.$('#number-container').hasClass('valid')) { + this.$('#request-sms, #request-voice').removeAttr('disabled'); + } else { + this.$('#request-sms, #request-voice').prop('disabled', 'disabled'); + } + }, + onChangeCode: function() { + if (!this.validateCode()) { + this.$('#code').addClass('invalid'); + } else { + this.$('#code').removeClass('invalid'); + } + }, + requestVoice: function() { + window.removeSetupMenuItems(); + this.$('#error').hide(); + var number = this.phoneView.validateNumber(); + if (number) { + this.accountManager + .requestVoiceVerification(number) + .catch(this.displayError.bind(this)); + this.$('#step2') + .addClass('in') + .fadeIn(); + } else { + this.$('#number-container').addClass('invalid'); + } + }, + requestSMSVerification: function() { + window.removeSetupMenuItems(); + $('#error').hide(); + var number = this.phoneView.validateNumber(); + if (number) { + this.accountManager + .requestSMSVerification(number) + .catch(this.displayError.bind(this)); + this.$('#step2') + .addClass('in') + .fadeIn(); + } else { + this.$('#number-container').addClass('invalid'); + } + }, + }); })(); diff --git a/js/views/timestamp_view.js b/js/views/timestamp_view.js index 755538d8b..2d39e4e82 100644 --- a/js/views/timestamp_view.js +++ b/js/views/timestamp_view.js @@ -1,88 +1,102 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.TimestampView = Whisper.View.extend({ - initialize: function(options) { - extension.windows.onClosed(this.clearTimeout.bind(this)); - }, - update: function() { - this.clearTimeout(); - var millis_now = Date.now(); - var millis = this.$el.data('timestamp'); - if (millis === "") { - return; - } - if (millis >= millis_now) { - millis = millis_now; - } - var result = this.getRelativeTimeSpanString(millis); - this.$el.text(result); + Whisper.TimestampView = Whisper.View.extend({ + initialize: function(options) { + extension.windows.onClosed(this.clearTimeout.bind(this)); + }, + update: function() { + this.clearTimeout(); + var millis_now = Date.now(); + var millis = this.$el.data('timestamp'); + if (millis === '') { + return; + } + if (millis >= millis_now) { + millis = millis_now; + } + var result = this.getRelativeTimeSpanString(millis); + this.$el.text(result); - var timestamp = moment(millis); - this.$el.attr('title', timestamp.format('llll')); + var timestamp = moment(millis); + this.$el.attr('title', timestamp.format('llll')); - var millis_since = millis_now - millis; - if (this.delay) { - if (this.delay < 0) { this.delay = 1000; } - this.timeout = setTimeout(this.update.bind(this), this.delay); - } - }, - clearTimeout: function() { - clearTimeout(this.timeout); - }, - getRelativeTimeSpanString: function(timestamp_) { - // Convert to moment timestamp if it isn't already - var timestamp = moment(timestamp_), - now = moment(), - timediff = moment.duration(now - timestamp); - - if (timediff.years() > 0) { - this.delay = null; - return timestamp.format(this._format.y); - } else if (timediff.months() > 0 || timediff.days() > 6) { - this.delay = null; - return timestamp.format(this._format.M); - } else if (timediff.days() > 0) { - this.delay = moment(timestamp).add(timediff.days() + 1,'d').diff(now); - return timestamp.format(this._format.d); - } else if (timediff.hours() > 1) { - this.delay = moment(timestamp).add(timediff.hours() + 1,'h').diff(now); - return this.relativeTime(timediff.hours(), 'h'); - } else if (timediff.hours() === 1) { - this.delay = moment(timestamp).add(timediff.hours() + 1,'h').diff(now); - return this.relativeTime(timediff.hours(), 'h'); - } else if (timediff.minutes() > 1) { - this.delay = moment(timestamp).add(timediff.minutes() + 1,'m').diff(now); - return this.relativeTime(timediff.minutes(), 'm'); - } else if (timediff.minutes() === 1) { - this.delay = moment(timestamp).add(timediff.minutes() + 1,'m').diff(now); - return this.relativeTime(timediff.minutes(), 'm'); - } else { - this.delay = moment(timestamp).add(1,'m').diff(now); - return this.relativeTime(timediff.seconds(), 's'); - } - }, - relativeTime : function (number, string) { - return moment.duration(number, string).humanize(); - }, - _format: { - y: "ll", - M: i18n('timestampFormat_M') || "MMM D", - d: "ddd" + var millis_since = millis_now - millis; + if (this.delay) { + if (this.delay < 0) { + this.delay = 1000; } - }); - Whisper.ExtendedTimestampView = Whisper.TimestampView.extend({ - relativeTime : function (number, string, isFuture) { - return moment.duration(-1 * number, string).humanize(string !== 's'); - }, - _format: { - y: "lll", - M: (i18n('timestampFormat_M') || "MMM D") + ' LT', - d: "ddd LT" - } - }); + this.timeout = setTimeout(this.update.bind(this), this.delay); + } + }, + clearTimeout: function() { + clearTimeout(this.timeout); + }, + getRelativeTimeSpanString: function(timestamp_) { + // Convert to moment timestamp if it isn't already + var timestamp = moment(timestamp_), + now = moment(), + timediff = moment.duration(now - timestamp); + + if (timediff.years() > 0) { + this.delay = null; + return timestamp.format(this._format.y); + } else if (timediff.months() > 0 || timediff.days() > 6) { + this.delay = null; + return timestamp.format(this._format.M); + } else if (timediff.days() > 0) { + this.delay = moment(timestamp) + .add(timediff.days() + 1, 'd') + .diff(now); + return timestamp.format(this._format.d); + } else if (timediff.hours() > 1) { + this.delay = moment(timestamp) + .add(timediff.hours() + 1, 'h') + .diff(now); + return this.relativeTime(timediff.hours(), 'h'); + } else if (timediff.hours() === 1) { + this.delay = moment(timestamp) + .add(timediff.hours() + 1, 'h') + .diff(now); + return this.relativeTime(timediff.hours(), 'h'); + } else if (timediff.minutes() > 1) { + this.delay = moment(timestamp) + .add(timediff.minutes() + 1, 'm') + .diff(now); + return this.relativeTime(timediff.minutes(), 'm'); + } else if (timediff.minutes() === 1) { + this.delay = moment(timestamp) + .add(timediff.minutes() + 1, 'm') + .diff(now); + return this.relativeTime(timediff.minutes(), 'm'); + } else { + this.delay = moment(timestamp) + .add(1, 'm') + .diff(now); + return this.relativeTime(timediff.seconds(), 's'); + } + }, + relativeTime: function(number, string) { + return moment.duration(number, string).humanize(); + }, + _format: { + y: 'll', + M: i18n('timestampFormat_M') || 'MMM D', + d: 'ddd', + }, + }); + Whisper.ExtendedTimestampView = Whisper.TimestampView.extend({ + relativeTime: function(number, string, isFuture) { + return moment.duration(-1 * number, string).humanize(string !== 's'); + }, + _format: { + y: 'lll', + M: (i18n('timestampFormat_M') || 'MMM D') + ' LT', + d: 'ddd LT', + }, + }); })(); diff --git a/js/views/toast_view.js b/js/views/toast_view.js index d4712a355..de27cb753 100644 --- a/js/views/toast_view.js +++ b/js/views/toast_view.js @@ -1,28 +1,30 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.ToastView = Whisper.View.extend({ - className: 'toast', - templateName: 'toast', - initialize: function() { - this.$el.hide(); - }, + Whisper.ToastView = Whisper.View.extend({ + className: 'toast', + templateName: 'toast', + initialize: function() { + this.$el.hide(); + }, - close: function() { - this.$el.fadeOut(this.remove.bind(this)); - }, + close: function() { + this.$el.fadeOut(this.remove.bind(this)); + }, - render: function() { - this.$el.html(Mustache.render( - _.result(this, 'template', ''), - _.result(this, 'render_attributes', '') - )); - this.$el.show(); - setTimeout(this.close.bind(this), 2000); - } - }); + render: function() { + this.$el.html( + Mustache.render( + _.result(this, 'template', ''), + _.result(this, 'render_attributes', '') + ) + ); + this.$el.show(); + setTimeout(this.close.bind(this), 2000); + }, + }); })(); diff --git a/js/views/whisper_view.js b/js/views/whisper_view.js index 19b70ee89..44baa00b4 100644 --- a/js/views/whisper_view.js +++ b/js/views/whisper_view.js @@ -19,62 +19,68 @@ * 4. Provides some common functionality, e.g. confirmation dialog * */ -(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - Whisper.View = Backbone.View.extend({ - constructor: function() { - Backbone.View.apply(this, arguments); - Mustache.parse(_.result(this, 'template')); - }, - render_attributes: function() { - return _.result(this.model, 'attributes', {}); - }, - render_partials: function() { - return Whisper.View.Templates; - }, - template: function() { - if (this.templateName) { - return Whisper.View.Templates[this.templateName]; - } - return ''; - }, - render: function() { - var attrs = _.result(this, 'render_attributes', {}); - var template = _.result(this, 'template', ''); - var partials = _.result(this, 'render_partials', ''); - this.$el.html(Mustache.render(template, attrs, partials)); - return this; - }, - confirm: function(message, okText) { - return new Promise(function(resolve, reject) { - var dialog = new Whisper.ConfirmationDialogView({ - message: message, - okText: okText, - resolve: resolve, - reject: reject - }); - this.$el.append(dialog.el); - }.bind(this)); - }, - i18n_with_links: function() { - var args = Array.prototype.slice.call(arguments); - for (var i=1; i < args.length; ++i) { - args[i] = 'class="link" href="' + encodeURI(args[i]) + '" target="_blank"'; - } - return i18n(args[0], args.slice(1)); + Whisper.View = Backbone.View.extend( + { + constructor: function() { + Backbone.View.apply(this, arguments); + Mustache.parse(_.result(this, 'template')); + }, + render_attributes: function() { + return _.result(this.model, 'attributes', {}); + }, + render_partials: function() { + return Whisper.View.Templates; + }, + template: function() { + if (this.templateName) { + return Whisper.View.Templates[this.templateName]; } - },{ - // Class attributes - Templates: (function() { - var templates = {}; - $('script[type="text/x-tmpl-mustache"]').each(function(i, el) { - var $el = $(el); - var id = $el.attr('id'); - templates[id] = $el.html(); + return ''; + }, + render: function() { + var attrs = _.result(this, 'render_attributes', {}); + var template = _.result(this, 'template', ''); + var partials = _.result(this, 'render_partials', ''); + this.$el.html(Mustache.render(template, attrs, partials)); + return this; + }, + confirm: function(message, okText) { + return new Promise( + function(resolve, reject) { + var dialog = new Whisper.ConfirmationDialogView({ + message: message, + okText: okText, + resolve: resolve, + reject: reject, }); - return templates; - }()) - }); + this.$el.append(dialog.el); + }.bind(this) + ); + }, + i18n_with_links: function() { + var args = Array.prototype.slice.call(arguments); + for (var i = 1; i < args.length; ++i) { + args[i] = + 'class="link" href="' + encodeURI(args[i]) + '" target="_blank"'; + } + return i18n(args[0], args.slice(1)); + }, + }, + { + // Class attributes + Templates: (function() { + var templates = {}; + $('script[type="text/x-tmpl-mustache"]').each(function(i, el) { + var $el = $(el); + var id = $el.attr('id'); + templates[id] = $el.html(); + }); + return templates; + })(), + } + ); })(); diff --git a/js/wall_clock_listener.js b/js/wall_clock_listener.js index e9d6b7ddd..70ef8b031 100644 --- a/js/wall_clock_listener.js +++ b/js/wall_clock_listener.js @@ -2,26 +2,26 @@ * vim: ts=4:sw=4:expandtab */ -;(function () { - 'use strict'; - window.Whisper = window.Whisper || {}; +(function() { + 'use strict'; + window.Whisper = window.Whisper || {}; - var lastTime; - var interval = 1000; - var events; - function checkTime() { - var currentTime = Date.now(); - if (currentTime > (lastTime + interval * 2)) { - events.trigger('timetravel'); - } - lastTime = currentTime; + var lastTime; + var interval = 1000; + var events; + function checkTime() { + var currentTime = Date.now(); + if (currentTime > lastTime + interval * 2) { + events.trigger('timetravel'); } + lastTime = currentTime; + } - Whisper.WallClockListener = { - init: function(_events) { - events = _events; - lastTime = Date.now(); - setInterval(checkTime, interval); - } - }; -}()); + Whisper.WallClockListener = { + init: function(_events) { + events = _events; + lastTime = Date.now(); + setInterval(checkTime, interval); + }, + }; +})(); diff --git a/main.js b/main.js index 9f2d87a3b..51b556aff 100644 --- a/main.js +++ b/main.js @@ -6,13 +6,7 @@ const _ = require('lodash'); const electron = require('electron'); const semver = require('semver'); -const { - BrowserWindow, - app, - Menu, - shell, - ipcMain: ipc, -} = electron; +const { BrowserWindow, app, Menu, shell, ipcMain: ipc } = electron; const packageJson = require('./package.json'); @@ -27,7 +21,9 @@ const { createTemplate } = require('./app/menu'); GlobalErrors.addHandler(); const appUserModelId = `org.whispersystems.${packageJson.name}`; -console.log('Set Windows Application User Model ID (AUMID)', { appUserModelId }); +console.log('Set Windows Application User Model ID (AUMID)', { + appUserModelId, +}); app.setAppUserModelId(appUserModelId); // Keep a global reference of the window object, if you don't, the window will @@ -41,13 +37,13 @@ function getMainWindow() { // Tray icon and related objects let tray = null; const startInTray = process.argv.some(arg => arg === '--start-in-tray'); -const usingTrayIcon = startInTray || process.argv.some(arg => arg === '--use-tray-icon'); - +const usingTrayIcon = + startInTray || process.argv.some(arg => arg === '--use-tray-icon'); const config = require('./app/config'); -const importMode = process.argv.some(arg => arg === '--import') || config.get('import'); - +const importMode = + process.argv.some(arg => arg === '--import') || config.get('import'); const development = config.environment === 'development'; @@ -107,7 +103,12 @@ const WINDOWS_8 = '8.0.0'; const osRelease = os.release(); const polyfillNotifications = os.platform() === 'win32' && semver.lt(osRelease, WINDOWS_8); -console.log('OS Release:', osRelease, '- notifications polyfill?', polyfillNotifications); +console.log( + 'OS Release:', + osRelease, + '- notifications polyfill?', + polyfillNotifications +); function prepareURL(pathSegments) { return url.format({ @@ -146,7 +147,6 @@ function captureClicks(window) { window.webContents.on('new-window', handleUrl); } - const DEFAULT_WIDTH = 800; const DEFAULT_HEIGHT = 610; const MIN_WIDTH = 640; @@ -160,35 +160,50 @@ function isVisible(window, bounds) { const boundsHeight = _.get(bounds, 'height') || DEFAULT_HEIGHT; // requiring BOUNDS_BUFFER pixels on the left or right side - const rightSideClearOfLeftBound = (window.x + window.width >= boundsX + BOUNDS_BUFFER); - const leftSideClearOfRightBound = (window.x <= (boundsX + boundsWidth) - BOUNDS_BUFFER); + const rightSideClearOfLeftBound = + window.x + window.width >= boundsX + BOUNDS_BUFFER; + const leftSideClearOfRightBound = + window.x <= boundsX + boundsWidth - BOUNDS_BUFFER; // top can't be offscreen, and must show at least BOUNDS_BUFFER pixels at bottom const topClearOfUpperBound = window.y >= boundsY; - const topClearOfLowerBound = (window.y <= (boundsY + boundsHeight) - BOUNDS_BUFFER); + const topClearOfLowerBound = + window.y <= boundsY + boundsHeight - BOUNDS_BUFFER; - return rightSideClearOfLeftBound && + return ( + rightSideClearOfLeftBound && leftSideClearOfRightBound && topClearOfUpperBound && - topClearOfLowerBound; + topClearOfLowerBound + ); } function createWindow() { const { screen } = electron; - const windowOptions = Object.assign({ - show: !startInTray, // allow to start minimised in tray - width: DEFAULT_WIDTH, - height: DEFAULT_HEIGHT, - minWidth: MIN_WIDTH, - minHeight: MIN_HEIGHT, - autoHideMenuBar: false, - webPreferences: { - nodeIntegration: false, - // sandbox: true, - preload: path.join(__dirname, 'preload.js'), + const windowOptions = Object.assign( + { + show: !startInTray, // allow to start minimised in tray + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + minWidth: MIN_WIDTH, + minHeight: MIN_HEIGHT, + autoHideMenuBar: false, + webPreferences: { + nodeIntegration: false, + // sandbox: true, + preload: path.join(__dirname, 'preload.js'), + }, + icon: path.join(__dirname, 'images', 'icon_256.png'), }, - icon: path.join(__dirname, 'images', 'icon_256.png'), - }, _.pick(windowConfig, ['maximized', 'autoHideMenuBar', 'width', 'height', 'x', 'y'])); + _.pick(windowConfig, [ + 'maximized', + 'autoHideMenuBar', + 'width', + 'height', + 'x', + 'y', + ]) + ); if (!_.isNumber(windowOptions.width) || windowOptions.width < MIN_WIDTH) { windowOptions.width = DEFAULT_WIDTH; @@ -203,7 +218,7 @@ function createWindow() { delete windowOptions.autoHideMenuBar; } - const visibleOnAnyScreen = _.some(screen.getAllDisplays(), (display) => { + const visibleOnAnyScreen = _.some(screen.getAllDisplays(), display => { if (!_.isNumber(windowOptions.x) || !_.isNumber(windowOptions.y)) { return false; } @@ -220,7 +235,10 @@ function createWindow() { delete windowOptions.fullscreen; } - logger.info('Initializing BrowserWindow config: %s', JSON.stringify(windowOptions)); + logger.info( + 'Initializing BrowserWindow config: %s', + JSON.stringify(windowOptions) + ); // Create the browser window. mainWindow = new BrowserWindow(windowOptions); @@ -249,7 +267,10 @@ function createWindow() { windowConfig.fullscreen = true; } - logger.info('Updating BrowserWindow config: %s', JSON.stringify(windowConfig)); + logger.info( + 'Updating BrowserWindow config: %s', + JSON.stringify(windowConfig) + ); userConfig.set('window', windowConfig); } @@ -263,7 +284,7 @@ function createWindow() { }); // Ingested in preload.js via a sendSync call - ipc.on('locale-data', (event) => { + ipc.on('locale-data', event => { // eslint-disable-next-line no-param-reassign event.returnValue = locale.messages; }); @@ -271,7 +292,9 @@ function createWindow() { if (config.environment === 'test') { mainWindow.loadURL(prepareURL([__dirname, 'test', 'index.html'])); } else if (config.environment === 'test-lib') { - mainWindow.loadURL(prepareURL([__dirname, 'libtextsecure', 'test', 'index.html'])); + mainWindow.loadURL( + prepareURL([__dirname, 'libtextsecure', 'test', 'index.html']) + ); } else { mainWindow.loadURL(prepareURL([__dirname, 'background.html'])); } @@ -283,16 +306,19 @@ function createWindow() { captureClicks(mainWindow); - mainWindow.webContents.on('will-navigate', (e) => { + mainWindow.webContents.on('will-navigate', e => { logger.info('will-navigate'); e.preventDefault(); }); // Emitted when the window is about to be closed. - mainWindow.on('close', (e) => { + mainWindow.on('close', e => { // If the application is terminating, just do the default - if (windowState.shouldQuit() || - config.environment === 'test' || config.environment === 'test-lib') { + if ( + windowState.shouldQuit() || + config.environment === 'test' || + config.environment === 'test-lib' + ) { return; } @@ -337,7 +363,9 @@ function showSettings() { } function openReleaseNotes() { - shell.openExternal(`https://github.com/signalapp/Signal-Desktop/releases/tag/v${app.getVersion()}`); + shell.openExternal( + `https://github.com/signalapp/Signal-Desktop/releases/tag/v${app.getVersion()}` + ); } function openNewBugForm() { @@ -345,7 +373,9 @@ function openNewBugForm() { } function openSupportPage() { - shell.openExternal('https://support.signal.org/hc/en-us/categories/202319038-Desktop'); + shell.openExternal( + 'https://support.signal.org/hc/en-us/categories/202319038-Desktop' + ); } function openForums() { @@ -370,7 +400,6 @@ function setupAsStandalone() { } } - let aboutWindow; function showAbout() { if (aboutWindow) { @@ -416,38 +445,42 @@ app.on('ready', () => { // NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`: /* eslint-disable more/no-then */ let loggingSetupError; - logging.initialize().catch((error) => { - loggingSetupError = error; - }).then(async () => { - /* eslint-enable more/no-then */ - logger = logging.getLogger(); - logger.info('app ready'); + logging + .initialize() + .catch(error => { + loggingSetupError = error; + }) + .then(async () => { + /* eslint-enable more/no-then */ + logger = logging.getLogger(); + logger.info('app ready'); - if (loggingSetupError) { - logger.error('Problem setting up logging', loggingSetupError.stack); - } + if (loggingSetupError) { + logger.error('Problem setting up logging', loggingSetupError.stack); + } - if (!locale) { - const appLocale = process.env.NODE_ENV === 'test' ? 'en' : app.getLocale(); - locale = loadLocale({ appLocale, logger }); - } + if (!locale) { + const appLocale = + process.env.NODE_ENV === 'test' ? 'en' : app.getLocale(); + locale = loadLocale({ appLocale, logger }); + } - console.log('Ensure attachments directory exists'); - const userDataPath = app.getPath('userData'); - await Attachments.ensureDirectory(userDataPath); + console.log('Ensure attachments directory exists'); + const userDataPath = app.getPath('userData'); + await Attachments.ensureDirectory(userDataPath); - ready = true; + ready = true; - autoUpdate.initialize(getMainWindow, locale.messages); + autoUpdate.initialize(getMainWindow, locale.messages); - createWindow(); + createWindow(); - if (usingTrayIcon) { - tray = createTrayIcon(getMainWindow, locale.messages); - } + if (usingTrayIcon) { + tray = createTrayIcon(getMainWindow, locale.messages); + } - setupMenu(); - }); + setupMenu(); + }); }); function setupMenu(options) { @@ -472,7 +505,6 @@ function setupMenu(options) { Menu.setApplicationMenu(menu); } - app.on('before-quit', () => { windowState.markShouldQuit(); }); @@ -481,9 +513,11 @@ app.on('before-quit', () => { app.on('window-all-closed', () => { // On OS X it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin' || - config.environment === 'test' || - config.environment === 'test-lib') { + if ( + process.platform !== 'darwin' || + config.environment === 'test' || + config.environment === 'test-lib' + ) { app.quit(); } }); @@ -504,7 +538,7 @@ app.on('activate', () => { // Defense in depth. We never intend to open webviews, so this prevents it completely. app.on('web-contents-created', (createEvent, win) => { - win.on('will-attach-webview', (attachEvent) => { + win.on('will-attach-webview', attachEvent => { attachEvent.preventDefault(); }); }); @@ -523,7 +557,6 @@ ipc.on('add-setup-menu-items', () => { }); }); - ipc.on('draw-attention', () => { if (process.platform === 'darwin') { app.dock.bounce(); diff --git a/preload.js b/preload.js index 0822678c2..c8cb16df7 100644 --- a/preload.js +++ b/preload.js @@ -12,7 +12,6 @@ const { deferredToPromise } = require('./js/modules/deferred_to_promise'); const { app } = electron.remote; - window.PROTO_ROOT = 'protos'; window.config = require('url').parse(window.location.toString(), true).query; @@ -21,8 +20,7 @@ window.wrapDeferred = deferredToPromise; const ipc = electron.ipcRenderer; window.config.localeMessages = ipc.sendSync('locale-data'); -window.setBadgeCount = count => - ipc.send('set-badge-count', count); +window.setBadgeCount = count => ipc.send('set-badge-count', count); window.drawAttention = () => { console.log('draw attention'); @@ -44,8 +42,7 @@ window.restart = () => { ipc.send('restart'); }; -window.closeAbout = () => - ipc.send('close-about'); +window.closeAbout = () => ipc.send('close-about'); window.updateTrayIcon = unreadCount => ipc.send('update-tray-icon', unreadCount); @@ -70,11 +67,9 @@ ipc.on('show-settings', () => { Whisper.events.trigger('showSettings'); }); -window.addSetupMenuItems = () => - ipc.send('add-setup-menu-items'); +window.addSetupMenuItems = () => ipc.send('add-setup-menu-items'); -window.removeSetupMenuItems = () => - ipc.send('remove-setup-menu-items'); +window.removeSetupMenuItems = () => ipc.send('remove-setup-menu-items'); // We pull these dependencies in now, from here, because they have Node.js dependencies @@ -101,8 +96,7 @@ window.emojiData = require('emoji-datasource'); window.EmojiPanel = require('emoji-panel'); window.filesize = require('filesize'); window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance(); -window.libphonenumber.PhoneNumberFormat = - require('google-libphonenumber').PhoneNumberFormat; +window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat; window.loadImage = require('blueimp-load-image'); window.nodeBuffer = Buffer; @@ -136,11 +130,15 @@ window.moment.locale(locale); // ES2015+ modules const attachmentsPath = Attachments.getPath(app.getPath('userData')); -const getAbsoluteAttachmentPath = Attachments.createAbsolutePathGetter(attachmentsPath); +const getAbsoluteAttachmentPath = Attachments.createAbsolutePathGetter( + attachmentsPath +); const deleteAttachmentData = Attachments.createDeleter(attachmentsPath); const readAttachmentData = Attachments.createReader(attachmentsPath); const writeNewAttachmentData = Attachments.createWriterForNew(attachmentsPath); -const writeExistingAttachmentData = Attachments.createWriterForExisting(attachmentsPath); +const writeExistingAttachmentData = Attachments.createWriterForExisting( + attachmentsPath +); const loadAttachmentData = Attachment.loadData(readAttachmentData); @@ -151,8 +149,9 @@ const upgradeSchemaContext = { const upgradeMessageSchema = message => Message.upgradeSchema(message, upgradeSchemaContext); -const { getPlaceholderMigrations } = - require('./js/modules/migrations/get_placeholder_migrations'); +const { + getPlaceholderMigrations, +} = require('./js/modules/migrations/get_placeholder_migrations'); const { IdleDetector } = require('./js/modules/idle_detector'); window.Signal = {}; @@ -167,12 +166,12 @@ window.Signal.Logs = require('./js/modules/logs'); // React components const { Lightbox } = require('./ts/components/Lightbox'); const { LightboxGallery } = require('./ts/components/LightboxGallery'); -const { MediaGallery } = - require('./ts/components/conversation/media-gallery/MediaGallery'); +const { + MediaGallery, +} = require('./ts/components/conversation/media-gallery/MediaGallery'); const { Quote } = require('./ts/components/conversation/Quote'); -const MediaGalleryMessage = - require('./ts/components/conversation/media-gallery/types/Message'); +const MediaGalleryMessage = require('./ts/components/conversation/media-gallery/types/Message'); window.Signal.Components = { Lightbox, @@ -185,18 +184,20 @@ window.Signal.Components = { }; window.Signal.Migrations = {}; -window.Signal.Migrations.deleteAttachmentData = - Attachment.deleteData(deleteAttachmentData); +window.Signal.Migrations.deleteAttachmentData = Attachment.deleteData( + deleteAttachmentData +); window.Signal.Migrations.getPlaceholderMigrations = getPlaceholderMigrations; -window.Signal.Migrations.writeMessageAttachments = - Message.createAttachmentDataWriter(writeExistingAttachmentData); +window.Signal.Migrations.writeMessageAttachments = Message.createAttachmentDataWriter( + writeExistingAttachmentData +); window.Signal.Migrations.getAbsoluteAttachmentPath = getAbsoluteAttachmentPath; window.Signal.Migrations.loadAttachmentData = loadAttachmentData; -window.Signal.Migrations.loadMessage = Message.createAttachmentLoader(loadAttachmentData); -window.Signal.Migrations.Migrations0DatabaseWithAttachmentData = - require('./js/modules/migrations/migrations_0_database_with_attachment_data'); -window.Signal.Migrations.Migrations1DatabaseWithoutAttachmentData = - require('./js/modules/migrations/migrations_1_database_without_attachment_data'); +window.Signal.Migrations.loadMessage = Message.createAttachmentLoader( + loadAttachmentData +); +window.Signal.Migrations.Migrations0DatabaseWithAttachmentData = require('./js/modules/migrations/migrations_0_database_with_attachment_data'); +window.Signal.Migrations.Migrations1DatabaseWithoutAttachmentData = require('./js/modules/migrations/migrations_1_database_without_attachment_data'); window.Signal.Migrations.upgradeMessageSchema = upgradeMessageSchema; window.Signal.OS = require('./js/modules/os'); @@ -218,8 +219,7 @@ window.Signal.Views.Initialization = require('./js/modules/views/initialization' window.Signal.Workflow = {}; window.Signal.Workflow.IdleDetector = IdleDetector; -window.Signal.Workflow.MessageDataMigrator = - require('./js/modules/messages_data_migrator'); +window.Signal.Workflow.MessageDataMigrator = require('./js/modules/messages_data_migrator'); // We pull this in last, because the native module involved appears to be sensitive to // /tmp mounted as noexec on Linux. diff --git a/prepare_beta_build.js b/prepare_beta_build.js index 203358543..693bdfcec 100644 --- a/prepare_beta_build.js +++ b/prepare_beta_build.js @@ -3,7 +3,6 @@ const _ = require('lodash'); const packageJson = require('./package.json'); - const { version } = packageJson; const beta = /beta/; @@ -37,7 +36,6 @@ const STARTUP_WM_CLASS_PATH = 'build.linux.desktop.StartupWMClass'; const PRODUCTION_STARTUP_WM_CLASS = 'Signal'; const BETA_STARTUP_WM_CLASS = 'Signal Beta'; - // ------- function checkValue(object, objectPath, expected) { diff --git a/prepare_import_build.js b/prepare_import_build.js index b0bcdc2d0..b20aacdc6 100644 --- a/prepare_import_build.js +++ b/prepare_import_build.js @@ -56,5 +56,8 @@ _.set(packageJson, WIN_ASSET_PATH, WIN_ASSET_END_VALUE); // --- -fs.writeFileSync('./config/default.json', JSON.stringify(defaultConfig, null, ' ')); +fs.writeFileSync( + './config/default.json', + JSON.stringify(defaultConfig, null, ' ') +); fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, ' ')); diff --git a/styleguide.config.js b/styleguide.config.js index 30c620634..03aee37b7 100644 --- a/styleguide.config.js +++ b/styleguide.config.js @@ -2,7 +2,6 @@ const webpack = require('webpack'); const path = require('path'); const typescriptSupport = require('react-docgen-typescript'); - const propsParser = typescriptSupport.withCustomConfig('./tsconfig.json').parse; module.exports = { @@ -37,9 +36,7 @@ module.exports = { // Exposes necessary utilities in the global scope for all readme code snippets util: 'ts/styleguide/StyleGuideUtil', }, - contextDependencies: [ - path.join(__dirname, 'ts/styleguide'), - ], + contextDependencies: [path.join(__dirname, 'ts/styleguide')], // We don't want one long, single page pagePerSection: true, // Expose entire repository to the styleguidist server, primarily for stylesheets @@ -49,11 +46,13 @@ module.exports = { // https://react-styleguidist.js.org/docs/configuration.html#template template: { head: { - links: [{ - rel: 'stylesheet', - type: 'text/css', - href: '/stylesheets/manifest.css', - }], + links: [ + { + rel: 'stylesheet', + type: 'text/css', + href: '/stylesheets/manifest.css', + }, + ], }, body: { // Brings in all the necessary components to boostrap Backbone views @@ -157,10 +156,7 @@ module.exports = { resolve: { // Necessary to enable the absolute path used in the context option above - modules: [ - __dirname, - path.join(__dirname, 'node_modules'), - ], + modules: [__dirname, path.join(__dirname, 'node_modules')], extensions: ['.tsx'], }, @@ -168,7 +164,7 @@ module.exports = { rules: [ { test: /\.tsx?$/, - loader: 'ts-loader' + loader: 'ts-loader', }, { // To test handling of attachments, we need arraybuffers in memory diff --git a/test/.eslintrc.js b/test/.eslintrc.js index 375e170fb..10d3dec2f 100644 --- a/test/.eslintrc.js +++ b/test/.eslintrc.js @@ -7,7 +7,7 @@ module.exports = { }, globals: { - assert: true + assert: true, }, parserOptions: { @@ -16,11 +16,14 @@ module.exports = { rules: { // We still get the value of this rule, it just allows for dev deps - 'import/no-extraneous-dependencies': ['error', { - devDependencies: true - }], + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, + }, + ], // We want to keep each test structured the same, even if its contents are tiny 'arrow-body-style': 'off', - } + }, }; diff --git a/test/_test.js b/test/_test.js index 183644cf5..e7306487f 100644 --- a/test/_test.js +++ b/test/_test.js @@ -27,7 +27,7 @@ window.PROTO_ROOT = '../protos'; result: false, message: err.message, stack: err.stack, - titles: flattenTitles(test) + titles: flattenTitles(test), }); }); @@ -37,10 +37,10 @@ window.PROTO_ROOT = '../protos'; SauceReporter.prototype = OriginalReporter.prototype; mocha.reporter(SauceReporter); -}()); +})(); // Override the database id. -window.Whisper = window.Whisper || {}; +window.Whisper = window.Whisper || {}; window.Whisper.Database = window.Whisper.Database || {}; Whisper.Database.id = 'test'; @@ -49,21 +49,23 @@ Whisper.Database.id = 'test'; */ function assertEqualArrayBuffers(ab1, ab2) { assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2)); -}; +} function hexToArrayBuffer(str) { var ret = new ArrayBuffer(str.length / 2); var array = new Uint8Array(ret); - for (var i = 0; i < str.length/2; i++) { - array[i] = parseInt(str.substr(i*2, 2), 16); + for (var i = 0; i < str.length / 2; i++) { + array[i] = parseInt(str.substr(i * 2, 2), 16); } return ret; -}; +} /* Delete the database before running any tests */ before(function(done) { var idbReq = indexedDB.deleteDatabase('test'); - idbReq.onsuccess = function() { done(); }; + idbReq.onsuccess = function() { + done(); + }; }); async function clearDatabase(done) { @@ -80,5 +82,5 @@ async function clearDatabase(done) { await messages.destroyAll(); if (done) { done(); - }; + } } diff --git a/test/app/attachments_test.js b/test/app/attachments_test.js index ce337356a..3abf3631c 100644 --- a/test/app/attachments_test.js +++ b/test/app/attachments_test.js @@ -4,8 +4,9 @@ const tmp = require('tmp'); const { assert } = require('chai'); const Attachments = require('../../app/attachments'); -const { stringToArrayBuffer } = require('../../js/modules/string_to_array_buffer'); - +const { + stringToArrayBuffer, +} = require('../../js/modules/string_to_array_buffer'); const PREFIX_LENGTH = 2; const NUM_SEPARATORS = 1; @@ -30,7 +31,9 @@ describe('Attachments', () => { 'Attachments_createWriterForNew' ); - const outputPath = await Attachments.createWriterForNew(tempDirectory)(input); + const outputPath = await Attachments.createWriterForNew(tempDirectory)( + input + ); const output = await fse.readFile(path.join(tempDirectory, outputPath)); assert.lengthOf(outputPath, PATH_LENGTH); @@ -57,13 +60,16 @@ describe('Attachments', () => { 'Attachments_createWriterForExisting' ); - const relativePath = Attachments.getRelativePath(Attachments.createName()); + const relativePath = Attachments.getRelativePath( + Attachments.createName() + ); const attachment = { path: relativePath, data: input, }; - const outputPath = - await Attachments.createWriterForExisting(tempDirectory)(attachment); + const outputPath = await Attachments.createWriterForExisting( + tempDirectory + )(attachment); const output = await fse.readFile(path.join(tempDirectory, outputPath)); assert.equal(outputPath, relativePath); @@ -84,16 +90,23 @@ describe('Attachments', () => { }); it('should read file from disk', async () => { - const tempDirectory = path.join(tempRootDirectory, 'Attachments_createReader'); + const tempDirectory = path.join( + tempRootDirectory, + 'Attachments_createReader' + ); - const relativePath = Attachments.getRelativePath(Attachments.createName()); + const relativePath = Attachments.getRelativePath( + Attachments.createName() + ); const fullPath = path.join(tempDirectory, relativePath); const input = stringToArrayBuffer('test string'); const inputBuffer = Buffer.from(input); await fse.ensureFile(fullPath); await fse.writeFile(fullPath, inputBuffer); - const output = await Attachments.createReader(tempDirectory)(relativePath); + const output = await Attachments.createReader(tempDirectory)( + relativePath + ); assert.deepEqual(input, output); }); @@ -110,9 +123,14 @@ describe('Attachments', () => { }); it('should delete file from disk', async () => { - const tempDirectory = path.join(tempRootDirectory, 'Attachments_createDeleter'); + const tempDirectory = path.join( + tempRootDirectory, + 'Attachments_createDeleter' + ); - const relativePath = Attachments.getRelativePath(Attachments.createName()); + const relativePath = Attachments.getRelativePath( + Attachments.createName() + ); const fullPath = path.join(tempDirectory, relativePath); const input = stringToArrayBuffer('test string'); @@ -134,7 +152,8 @@ describe('Attachments', () => { describe('getRelativePath', () => { it('should return correct path', () => { - const name = '608ce3bc536edbf7637a6aeb6040bdfec49349140c0dd43e97c7ce263b15ff7e'; + const name = + '608ce3bc536edbf7637a6aeb6040bdfec49349140c0dd43e97c7ce263b15ff7e'; assert.lengthOf(Attachments.getRelativePath(name), PATH_LENGTH); }); }); diff --git a/test/app/logging_test.js b/test/app/logging_test.js index 3cd1383ce..82f6cd723 100644 --- a/test/app/logging_test.js +++ b/test/app/logging_test.js @@ -26,7 +26,7 @@ describe('app/logging', () => { basePath = tmpDir.name; }); - afterEach((done) => { + afterEach(done => { // we need the unsafe option to recursively remove the directory tmpDir.removeCallback(done); }); @@ -149,9 +149,11 @@ describe('app/logging', () => { ].join('\n'); const target = path.join(basePath, 'log.log'); - const files = [{ - path: target, - }]; + const files = [ + { + path: target, + }, + ]; fs.writeFileSync(target, contents); @@ -172,9 +174,11 @@ describe('app/logging', () => { ].join('\n'); const target = path.join(basePath, 'log.log'); - const files = [{ - path: target, - }]; + const files = [ + { + path: target, + }, + ]; fs.writeFileSync(target, contents); @@ -187,11 +191,16 @@ describe('app/logging', () => { describe('#fetchLog', () => { it('returns error if file does not exist', () => { const target = 'random_file'; - return fetchLog(target).then(() => { - throw new Error('Expected an error!'); - }, (error) => { - expect(error).to.have.property('message').that.match(/random_file/); - }); + return fetchLog(target).then( + () => { + throw new Error('Expected an error!'); + }, + error => { + expect(error) + .to.have.property('message') + .that.match(/random_file/); + } + ); }); it('returns empty array if file has no valid JSON lines', () => { const contents = 'line 1\nline2\n'; @@ -200,7 +209,7 @@ describe('app/logging', () => { fs.writeFileSync(target, contents); - return fetchLog(target).then((result) => { + return fetchLog(target).then(result => { expect(result).to.deep.equal(expected); }); }); @@ -222,21 +231,24 @@ describe('app/logging', () => { }), '', ].join('\n'); - const expected = [{ - level: 1, - time: 2, - msg: 3, - }, { - level: 2, - time: 3, - msg: 4, - }]; + const expected = [ + { + level: 1, + time: 2, + msg: 3, + }, + { + level: 2, + time: 3, + msg: 4, + }, + ]; const target = path.join(basePath, 'test.log'); fs.writeFileSync(target, contents); - return fetchLog(target).then((result) => { + return fetchLog(target).then(result => { expect(result).to.deep.equal(expected); }); }); @@ -244,7 +256,7 @@ describe('app/logging', () => { describe('#fetch', () => { it('returns single entry if no files', () => { - return fetch(basePath).then((results) => { + return fetch(basePath).then(results => { expect(results).to.have.length(1); expect(results[0].msg).to.match(/Loaded this list/); }); @@ -263,7 +275,7 @@ describe('app/logging', () => { fs.writeFileSync(path.join(basePath, 'first.log'), first); fs.writeFileSync(path.join(basePath, 'second.log'), second); - return fetch(basePath).then((results) => { + return fetch(basePath).then(results => { expect(results).to.have.length(4); expect(results[0].msg).to.equal(1); expect(results[1].msg).to.equal(2); diff --git a/test/app/menu_test.js b/test/app/menu_test.js index 544a8d1ed..a29a1838a 100644 --- a/test/app/menu_test.js +++ b/test/app/menu_test.js @@ -3,7 +3,6 @@ const { assert } = require('chai'); const SignalMenu = require('../../app/menu'); const { load: loadLocale } = require('../../app/locale'); - const PLATFORMS = [ { label: 'macOS', @@ -37,7 +36,7 @@ describe('SignalMenu', () => { describe('createTemplate', () => { PLATFORMS.forEach(({ label, platform, fixtures }) => { context(label, () => { - INCLUDE_SETUP_OPTIONS.forEach((includeSetup) => { + INCLUDE_SETUP_OPTIONS.forEach(includeSetup => { const prefix = includeSetup ? 'with' : 'without'; context(`${prefix} setup options`, () => { it('should return correct template', () => { @@ -65,7 +64,9 @@ describe('SignalMenu', () => { const { messages } = loadLocale({ appLocale, logger }); const actual = SignalMenu.createTemplate(options, messages); - const fixturePath = includeSetup ? fixtures.setup : fixtures.default; + const fixturePath = includeSetup + ? fixtures.setup + : fixtures.default; // eslint-disable-next-line global-require, import/no-dynamic-require const fixture = require(fixturePath); assert.deepEqual(actual, fixture); diff --git a/test/backup_test.js b/test/backup_test.js index 099e4d077..c5e591fa4 100644 --- a/test/backup_test.js +++ b/test/backup_test.js @@ -9,7 +9,7 @@ describe('Backup', () => { describe('_sanitizeFileName', () => { it('leaves a basic string alone', () => { - const initial = 'Hello, how are you #5 (\'fine\' + great).jpg'; + const initial = "Hello, how are you #5 ('fine' + great).jpg"; const expected = initial; assert.strictEqual(Signal.Backup._sanitizeFileName(initial), expected); }); @@ -29,7 +29,8 @@ describe('Backup', () => { }); it('handles a file with a long extension', () => { - const initial = '0123456789012345678901234567890123456789.01234567890123456789'; + const initial = + '0123456789012345678901234567890123456789.01234567890123456789'; const expected = '012345678901234567890123456789'; assert.strictEqual(Signal.Backup._trimFileName(initial), expected); }); @@ -165,7 +166,10 @@ describe('Backup', () => { id: 'id', }; const expected = '123 (012345678901234567890123456789 id)'; - assert.strictEqual(Signal.Backup._getConversationDirName(conversation), expected); + assert.strictEqual( + Signal.Backup._getConversationDirName(conversation), + expected + ); }); it('uses just id if name is not available', () => { @@ -174,7 +178,10 @@ describe('Backup', () => { id: 'id', }; const expected = '123 (id)'; - assert.strictEqual(Signal.Backup._getConversationDirName(conversation), expected); + assert.strictEqual( + Signal.Backup._getConversationDirName(conversation), + expected + ); }); it('uses inactive for missing active_at', () => { @@ -183,7 +190,10 @@ describe('Backup', () => { id: 'id', }; const expected = 'inactive (name id)'; - assert.strictEqual(Signal.Backup._getConversationDirName(conversation), expected); + assert.strictEqual( + Signal.Backup._getConversationDirName(conversation), + expected + ); }); }); @@ -229,23 +239,45 @@ describe('Backup', () => { describe('end-to-end', () => { it('exports then imports to produce the same data we started with', async () => { - const { - attachmentsPath, - fse, - glob, - path, - tmp, - } = window.test; + const { attachmentsPath, fse, glob, path, tmp } = window.test; const { upgradeMessageSchema, loadAttachmentData, } = window.Signal.Migrations; const key = new Uint8Array([ - 1, 3, 4, 5, 6, 7, 8, 11, - 23, 34, 1, 34, 3, 5, 45, 45, - 1, 3, 4, 5, 6, 7, 8, 11, - 23, 34, 1, 34, 3, 5, 45, 45, + 1, + 3, + 4, + 5, + 6, + 7, + 8, + 11, + 23, + 34, + 1, + 34, + 3, + 5, + 45, + 45, + 1, + 3, + 4, + 5, + 6, + 7, + 8, + 11, + 23, + 34, + 1, + 34, + 3, + 5, + 45, + 45, ]); const attachmentsPattern = path.join(attachmentsPath, '**'); @@ -279,7 +311,7 @@ describe('Backup', () => { // glob returns only /. We normalize to / separators for our manipulations. const normalizedBase = attachmentsPath.replace(/\\/g, '/'); function removeDirs(dirs) { - return _.filter(dirs, (fullDir) => { + return _.filter(dirs, fullDir => { const dir = fullDir.replace(normalizedBase, ''); return TWO_SLASHES.test(dir); }); @@ -291,7 +323,7 @@ describe('Backup', () => { return message; } - const wrappedMapper = async (attachment) => { + const wrappedMapper = async attachment => { if (!attachment || !attachment.thumbnail) { return attachment; } @@ -301,18 +333,21 @@ describe('Backup', () => { }); }; - const quotedAttachments = (message.quote && message.quote.attachments) || []; + const quotedAttachments = + (message.quote && message.quote.attachments) || []; return Object.assign({}, message, { quote: Object.assign({}, message.quote, { - attachments: await Promise.all(quotedAttachments.map(wrappedMapper)), + attachments: await Promise.all( + quotedAttachments.map(wrappedMapper) + ), }), }); }; } async function loadAllFilesFromDisk(message) { - const loadThumbnails = _mapQuotedAttachments((thumbnail) => { + const loadThumbnails = _mapQuotedAttachments(thumbnail => { // we want to be bulletproof to thumbnails without data if (!thumbnail.path) { return thumbnail; @@ -322,15 +357,12 @@ describe('Backup', () => { }); const promises = (message.attachments || []).map(attachment => - wrappedLoadAttachment(attachment)); - - return Object.assign( - {}, - await loadThumbnails(message), - { - attachments: await Promise.all(promises), - } + wrappedLoadAttachment(attachment) ); + + return Object.assign({}, await loadThumbnails(message), { + attachments: await Promise.all(promises), + }); } let backupDir; @@ -346,16 +378,46 @@ describe('Backup', () => { received_at: 1524185933350, timestamp: 1524185933350, errors: [], - attachments: [{ - contentType: 'image/gif', - fileName: 'sad_cat.gif', - data: new Uint8Array([ - 1, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, - ]).buffer, - }], + attachments: [ + { + contentType: 'image/gif', + fileName: 'sad_cat.gif', + data: new Uint8Array([ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + ]).buffer, + }, + ], hasAttachments: 1, hasFileAttachments: undefined, hasVisualMediaAttachments: 1, @@ -363,22 +425,53 @@ describe('Backup', () => { text: "Isn't it cute?", author: CONTACT_ONE_NUMBER, id: 12345678, - attachments: [{ - contentType: 'audio/mp3', - fileName: 'song.mp3', - }, { - contentType: 'image/gif', - fileName: 'happy_cat.gif', - thumbnail: { - contentType: 'image/png', - data: new Uint8Array([ - 2, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, - ]).buffer, + attachments: [ + { + contentType: 'audio/mp3', + fileName: 'song.mp3', }, - }], + { + contentType: 'image/gif', + fileName: 'happy_cat.gif', + thumbnail: { + contentType: 'image/png', + data: new Uint8Array([ + 2, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + ]).buffer, + }, + }, + ], }, }; @@ -401,18 +494,74 @@ describe('Backup', () => { profileAvatar: { contentType: 'image/jpeg', data: new Uint8Array([ - 3, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, + 3, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, ]).buffer, size: 64, }, profileKey: new Uint8Array([ - 4, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, - 1, 2, 3, 4, 5, 6, 7, 8, + 4, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, ]).buffer, profileName: 'Someone! 🤔', profileSharing: true, @@ -432,7 +581,9 @@ describe('Backup', () => { const conversationModel = new Whisper.Conversation(conversation); await window.wrapDeferred(conversationModel.save()); - console.log('Backup test: Ensure that all attachments were saved to disk'); + console.log( + 'Backup test: Ensure that all attachments were saved to disk' + ); const attachmentFiles = removeDirs(glob.sync(attachmentsPattern)); console.log({ attachmentFiles }); assert.strictEqual(ATTACHMENT_COUNT, attachmentFiles.length); @@ -447,7 +598,9 @@ describe('Backup', () => { const messageZipExists = fse.existsSync(zipPath); assert.strictEqual(true, messageZipExists); - console.log('Backup test: Ensure that all attachments made it to backup dir'); + console.log( + 'Backup test: Ensure that all attachments made it to backup dir' + ); const backupAttachmentPattern = path.join(backupDir, 'attachments/*'); const backupAttachments = glob.sync(backupAttachmentPattern); console.log({ backupAttachments }); @@ -460,7 +613,9 @@ describe('Backup', () => { await Signal.Backup.importFromDirectory(backupDir, { key }); console.log('Backup test: ensure that all attachments were imported'); - const recreatedAttachmentFiles = removeDirs(glob.sync(attachmentsPattern)); + const recreatedAttachmentFiles = removeDirs( + glob.sync(attachmentsPattern) + ); console.log({ recreatedAttachmentFiles }); assert.strictEqual(ATTACHMENT_COUNT, recreatedAttachmentFiles.length); assert.deepEqual(attachmentFiles, recreatedAttachmentFiles); @@ -472,15 +627,21 @@ describe('Backup', () => { const messageFromDB = removeId(messageCollection.at(0).attributes); const expectedMessage = omitUndefinedKeys(message); console.log({ messageFromDB, expectedMessage }); - assert.deepEqual( - messageFromDB, - expectedMessage - ); + assert.deepEqual(messageFromDB, expectedMessage); - console.log('Backup test: Check that all attachments were successfully imported'); - const messageWithAttachmentsFromDB = await loadAllFilesFromDisk(messageFromDB); - const expectedMessageWithAttachments = omitUndefinedKeys(messageWithAttachments); - console.log({ messageWithAttachmentsFromDB, expectedMessageWithAttachments }); + console.log( + 'Backup test: Check that all attachments were successfully imported' + ); + const messageWithAttachmentsFromDB = await loadAllFilesFromDisk( + messageFromDB + ); + const expectedMessageWithAttachments = omitUndefinedKeys( + messageWithAttachments + ); + console.log({ + messageWithAttachmentsFromDB, + expectedMessageWithAttachments, + }); assert.deepEqual( _.omit(messageWithAttachmentsFromDB, ['schemaVersion']), expectedMessageWithAttachments diff --git a/test/blanket_mocha.js b/test/blanket_mocha.js index ccab29937..f6b2b0ed1 100644 --- a/test/blanket_mocha.js +++ b/test/blanket_mocha.js @@ -1,7 +1,7 @@ /*! blanket - v1.1.5 */ -(function(define){ -/* +(function(define) { + /* Copyright (C) 2013 Ariya Hidayat Copyright (C) 2013 Thaddee Tyl Copyright (C) 2013 Mathias Bynens @@ -34,8 +34,8 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -/*jslint bitwise:true plusplus:true */ -/*global esprima:true, define:true, exports:true, window: true, + /*jslint bitwise:true plusplus:true */ + /*global esprima:true, define:true, exports:true, window: true, throwErrorTolerant: true, throwError: true, generateStatement: true, peek: true, parseAssignmentExpression: true, parseBlock: true, parseExpression: true, @@ -45,7 +45,7 @@ parseLeftHandSideExpression: true, parseUnaryExpression: true, parseStatement: true, parseSourceElement: true */ -(function (root, factory) { + (function(root, factory) { 'use strict'; // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, @@ -53,44 +53,44 @@ parseStatement: true, parseSourceElement: true */ /* istanbul ignore next */ if (typeof define === 'function' && define.amd) { - define(['exports'], factory); + define(['exports'], factory); } else if (typeof exports !== 'undefined') { - factory(exports); + factory(exports); } else { - factory((root.esprima = {})); + factory((root.esprima = {})); } -}(this, function (exports) { + })(this, function(exports) { 'use strict'; var Token, - TokenName, - FnExprTokens, - Syntax, - PropertyKind, - Messages, - Regex, - SyntaxTreeDelegate, - source, - strict, - index, - lineNumber, - lineStart, - length, - delegate, - lookahead, - state, - extra; + TokenName, + FnExprTokens, + Syntax, + PropertyKind, + Messages, + Regex, + SyntaxTreeDelegate, + source, + strict, + index, + lineNumber, + lineStart, + length, + delegate, + lookahead, + state, + extra; Token = { - BooleanLiteral: 1, - EOF: 2, - Identifier: 3, - Keyword: 4, - NullLiteral: 5, - NumericLiteral: 6, - Punctuator: 7, - StringLiteral: 8, - RegularExpression: 9 + BooleanLiteral: 1, + EOF: 2, + Identifier: 3, + Keyword: 4, + NullLiteral: 5, + NumericLiteral: 6, + Punctuator: 7, + StringLiteral: 8, + RegularExpression: 9, }; TokenName = {}; @@ -105,106 +105,169 @@ parseStatement: true, parseSourceElement: true */ TokenName[Token.RegularExpression] = 'RegularExpression'; // A function following one of those tokens is an expression. - FnExprTokens = ['(', '{', '[', 'in', 'typeof', 'instanceof', 'new', - 'return', 'case', 'delete', 'throw', 'void', - // assignment operators - '=', '+=', '-=', '*=', '/=', '%=', '<<=', '>>=', '>>>=', - '&=', '|=', '^=', ',', - // binary/unary operators - '+', '-', '*', '/', '%', '++', '--', '<<', '>>', '>>>', '&', - '|', '^', '!', '~', '&&', '||', '?', ':', '===', '==', '>=', - '<=', '<', '>', '!=', '!==']; + FnExprTokens = [ + '(', + '{', + '[', + 'in', + 'typeof', + 'instanceof', + 'new', + 'return', + 'case', + 'delete', + 'throw', + 'void', + // assignment operators + '=', + '+=', + '-=', + '*=', + '/=', + '%=', + '<<=', + '>>=', + '>>>=', + '&=', + '|=', + '^=', + ',', + // binary/unary operators + '+', + '-', + '*', + '/', + '%', + '++', + '--', + '<<', + '>>', + '>>>', + '&', + '|', + '^', + '!', + '~', + '&&', + '||', + '?', + ':', + '===', + '==', + '>=', + '<=', + '<', + '>', + '!=', + '!==', + ]; Syntax = { - AssignmentExpression: 'AssignmentExpression', - ArrayExpression: 'ArrayExpression', - BlockStatement: 'BlockStatement', - BinaryExpression: 'BinaryExpression', - BreakStatement: 'BreakStatement', - CallExpression: 'CallExpression', - CatchClause: 'CatchClause', - ConditionalExpression: 'ConditionalExpression', - ContinueStatement: 'ContinueStatement', - DoWhileStatement: 'DoWhileStatement', - DebuggerStatement: 'DebuggerStatement', - EmptyStatement: 'EmptyStatement', - ExpressionStatement: 'ExpressionStatement', - ForStatement: 'ForStatement', - ForInStatement: 'ForInStatement', - FunctionDeclaration: 'FunctionDeclaration', - FunctionExpression: 'FunctionExpression', - Identifier: 'Identifier', - IfStatement: 'IfStatement', - Literal: 'Literal', - LabeledStatement: 'LabeledStatement', - LogicalExpression: 'LogicalExpression', - MemberExpression: 'MemberExpression', - NewExpression: 'NewExpression', - ObjectExpression: 'ObjectExpression', - Program: 'Program', - Property: 'Property', - ReturnStatement: 'ReturnStatement', - SequenceExpression: 'SequenceExpression', - SwitchStatement: 'SwitchStatement', - SwitchCase: 'SwitchCase', - ThisExpression: 'ThisExpression', - ThrowStatement: 'ThrowStatement', - TryStatement: 'TryStatement', - UnaryExpression: 'UnaryExpression', - UpdateExpression: 'UpdateExpression', - VariableDeclaration: 'VariableDeclaration', - VariableDeclarator: 'VariableDeclarator', - WhileStatement: 'WhileStatement', - WithStatement: 'WithStatement' + AssignmentExpression: 'AssignmentExpression', + ArrayExpression: 'ArrayExpression', + BlockStatement: 'BlockStatement', + BinaryExpression: 'BinaryExpression', + BreakStatement: 'BreakStatement', + CallExpression: 'CallExpression', + CatchClause: 'CatchClause', + ConditionalExpression: 'ConditionalExpression', + ContinueStatement: 'ContinueStatement', + DoWhileStatement: 'DoWhileStatement', + DebuggerStatement: 'DebuggerStatement', + EmptyStatement: 'EmptyStatement', + ExpressionStatement: 'ExpressionStatement', + ForStatement: 'ForStatement', + ForInStatement: 'ForInStatement', + FunctionDeclaration: 'FunctionDeclaration', + FunctionExpression: 'FunctionExpression', + Identifier: 'Identifier', + IfStatement: 'IfStatement', + Literal: 'Literal', + LabeledStatement: 'LabeledStatement', + LogicalExpression: 'LogicalExpression', + MemberExpression: 'MemberExpression', + NewExpression: 'NewExpression', + ObjectExpression: 'ObjectExpression', + Program: 'Program', + Property: 'Property', + ReturnStatement: 'ReturnStatement', + SequenceExpression: 'SequenceExpression', + SwitchStatement: 'SwitchStatement', + SwitchCase: 'SwitchCase', + ThisExpression: 'ThisExpression', + ThrowStatement: 'ThrowStatement', + TryStatement: 'TryStatement', + UnaryExpression: 'UnaryExpression', + UpdateExpression: 'UpdateExpression', + VariableDeclaration: 'VariableDeclaration', + VariableDeclarator: 'VariableDeclarator', + WhileStatement: 'WhileStatement', + WithStatement: 'WithStatement', }; PropertyKind = { - Data: 1, - Get: 2, - Set: 4 + Data: 1, + Get: 2, + Set: 4, }; // Error messages should be identical to V8. Messages = { - UnexpectedToken: 'Unexpected token %0', - UnexpectedNumber: 'Unexpected number', - UnexpectedString: 'Unexpected string', - UnexpectedIdentifier: 'Unexpected identifier', - UnexpectedReserved: 'Unexpected reserved word', - UnexpectedEOS: 'Unexpected end of input', - NewlineAfterThrow: 'Illegal newline after throw', - InvalidRegExp: 'Invalid regular expression', - UnterminatedRegExp: 'Invalid regular expression: missing /', - InvalidLHSInAssignment: 'Invalid left-hand side in assignment', - InvalidLHSInForIn: 'Invalid left-hand side in for-in', - MultipleDefaultsInSwitch: 'More than one default clause in switch statement', - NoCatchOrFinally: 'Missing catch or finally after try', - UnknownLabel: 'Undefined label \'%0\'', - Redeclaration: '%0 \'%1\' has already been declared', - IllegalContinue: 'Illegal continue statement', - IllegalBreak: 'Illegal break statement', - IllegalReturn: 'Illegal return statement', - StrictModeWith: 'Strict mode code may not include a with statement', - StrictCatchVariable: 'Catch variable may not be eval or arguments in strict mode', - StrictVarName: 'Variable name may not be eval or arguments in strict mode', - StrictParamName: 'Parameter name eval or arguments is not allowed in strict mode', - StrictParamDupe: 'Strict mode function may not have duplicate parameter names', - StrictFunctionName: 'Function name may not be eval or arguments in strict mode', - StrictOctalLiteral: 'Octal literals are not allowed in strict mode.', - StrictDelete: 'Delete of an unqualified identifier in strict mode.', - StrictDuplicateProperty: 'Duplicate data property in object literal not allowed in strict mode', - AccessorDataProperty: 'Object literal may not have data and accessor property with the same name', - AccessorGetSet: 'Object literal may not have multiple get/set accessors with the same name', - StrictLHSAssignment: 'Assignment to eval or arguments is not allowed in strict mode', - StrictLHSPostfix: 'Postfix increment/decrement may not have eval or arguments operand in strict mode', - StrictLHSPrefix: 'Prefix increment/decrement may not have eval or arguments operand in strict mode', - StrictReservedWord: 'Use of future reserved word in strict mode' + UnexpectedToken: 'Unexpected token %0', + UnexpectedNumber: 'Unexpected number', + UnexpectedString: 'Unexpected string', + UnexpectedIdentifier: 'Unexpected identifier', + UnexpectedReserved: 'Unexpected reserved word', + UnexpectedEOS: 'Unexpected end of input', + NewlineAfterThrow: 'Illegal newline after throw', + InvalidRegExp: 'Invalid regular expression', + UnterminatedRegExp: 'Invalid regular expression: missing /', + InvalidLHSInAssignment: 'Invalid left-hand side in assignment', + InvalidLHSInForIn: 'Invalid left-hand side in for-in', + MultipleDefaultsInSwitch: + 'More than one default clause in switch statement', + NoCatchOrFinally: 'Missing catch or finally after try', + UnknownLabel: "Undefined label '%0'", + Redeclaration: "%0 '%1' has already been declared", + IllegalContinue: 'Illegal continue statement', + IllegalBreak: 'Illegal break statement', + IllegalReturn: 'Illegal return statement', + StrictModeWith: 'Strict mode code may not include a with statement', + StrictCatchVariable: + 'Catch variable may not be eval or arguments in strict mode', + StrictVarName: + 'Variable name may not be eval or arguments in strict mode', + StrictParamName: + 'Parameter name eval or arguments is not allowed in strict mode', + StrictParamDupe: + 'Strict mode function may not have duplicate parameter names', + StrictFunctionName: + 'Function name may not be eval or arguments in strict mode', + StrictOctalLiteral: 'Octal literals are not allowed in strict mode.', + StrictDelete: 'Delete of an unqualified identifier in strict mode.', + StrictDuplicateProperty: + 'Duplicate data property in object literal not allowed in strict mode', + AccessorDataProperty: + 'Object literal may not have data and accessor property with the same name', + AccessorGetSet: + 'Object literal may not have multiple get/set accessors with the same name', + StrictLHSAssignment: + 'Assignment to eval or arguments is not allowed in strict mode', + StrictLHSPostfix: + 'Postfix increment/decrement may not have eval or arguments operand in strict mode', + StrictLHSPrefix: + 'Prefix increment/decrement may not have eval or arguments operand in strict mode', + StrictReservedWord: 'Use of future reserved word in strict mode', }; // See also tools/generate-unicode-regex.py. Regex = { - NonAsciiIdentifierStart: new RegExp('[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F0\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]'), - NonAsciiIdentifierPart: new RegExp('[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0\u08A2-\u08AC\u08E4-\u08FE\u0900-\u0963\u0966-\u096F\u0971-\u0977\u0979-\u097F\u0981-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C01-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58\u0C59\u0C60-\u0C63\u0C66-\u0C6F\u0C82\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D02\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D60-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F0\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191C\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF6\u1D00-\u1DE6\u1DFC-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA697\uA69F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7B\uAA80-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE26\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]') + NonAsciiIdentifierStart: new RegExp( + '[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F0\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]' + ), + NonAsciiIdentifierPart: new RegExp( + '[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0\u08A2-\u08AC\u08E4-\u08FE\u0900-\u0963\u0966-\u096F\u0971-\u0977\u0979-\u097F\u0981-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C01-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58\u0C59\u0C60-\u0C63\u0C66-\u0C6F\u0C82\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D02\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D60-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F0\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191C\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF6\u1D00-\u1DE6\u1DFC-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA697\uA69F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7B\uAA80-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE26\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]' + ), }; // Ensure the condition is true, otherwise throw an error. @@ -213,75 +276,107 @@ parseStatement: true, parseSourceElement: true */ // Do NOT use this to enforce a certain condition on any user input. function assert(condition, message) { - /* istanbul ignore if */ - if (!condition) { - throw new Error('ASSERT: ' + message); - } + /* istanbul ignore if */ + if (!condition) { + throw new Error('ASSERT: ' + message); + } } function isDecimalDigit(ch) { - return (ch >= 48 && ch <= 57); // 0..9 + return ch >= 48 && ch <= 57; // 0..9 } function isHexDigit(ch) { - return '0123456789abcdefABCDEF'.indexOf(ch) >= 0; + return '0123456789abcdefABCDEF'.indexOf(ch) >= 0; } function isOctalDigit(ch) { - return '01234567'.indexOf(ch) >= 0; + return '01234567'.indexOf(ch) >= 0; } - // 7.2 White Space function isWhiteSpace(ch) { - return (ch === 0x20) || (ch === 0x09) || (ch === 0x0B) || (ch === 0x0C) || (ch === 0xA0) || - (ch >= 0x1680 && [0x1680, 0x180E, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000, 0xFEFF].indexOf(ch) >= 0); + return ( + ch === 0x20 || + ch === 0x09 || + ch === 0x0b || + ch === 0x0c || + ch === 0xa0 || + (ch >= 0x1680 && + [ + 0x1680, + 0x180e, + 0x2000, + 0x2001, + 0x2002, + 0x2003, + 0x2004, + 0x2005, + 0x2006, + 0x2007, + 0x2008, + 0x2009, + 0x200a, + 0x202f, + 0x205f, + 0x3000, + 0xfeff, + ].indexOf(ch) >= 0) + ); } // 7.3 Line Terminators function isLineTerminator(ch) { - return (ch === 0x0A) || (ch === 0x0D) || (ch === 0x2028) || (ch === 0x2029); + return ch === 0x0a || ch === 0x0d || ch === 0x2028 || ch === 0x2029; } // 7.6 Identifier Names and Identifiers function isIdentifierStart(ch) { - return (ch === 0x24) || (ch === 0x5F) || // $ (dollar) and _ (underscore) - (ch >= 0x41 && ch <= 0x5A) || // A..Z - (ch >= 0x61 && ch <= 0x7A) || // a..z - (ch === 0x5C) || // \ (backslash) - ((ch >= 0x80) && Regex.NonAsciiIdentifierStart.test(String.fromCharCode(ch))); + return ( + ch === 0x24 || + ch === 0x5f || // $ (dollar) and _ (underscore) + (ch >= 0x41 && ch <= 0x5a) || // A..Z + (ch >= 0x61 && ch <= 0x7a) || // a..z + ch === 0x5c || // \ (backslash) + (ch >= 0x80 && + Regex.NonAsciiIdentifierStart.test(String.fromCharCode(ch))) + ); } function isIdentifierPart(ch) { - return (ch === 0x24) || (ch === 0x5F) || // $ (dollar) and _ (underscore) - (ch >= 0x41 && ch <= 0x5A) || // A..Z - (ch >= 0x61 && ch <= 0x7A) || // a..z - (ch >= 0x30 && ch <= 0x39) || // 0..9 - (ch === 0x5C) || // \ (backslash) - ((ch >= 0x80) && Regex.NonAsciiIdentifierPart.test(String.fromCharCode(ch))); + return ( + ch === 0x24 || + ch === 0x5f || // $ (dollar) and _ (underscore) + (ch >= 0x41 && ch <= 0x5a) || // A..Z + (ch >= 0x61 && ch <= 0x7a) || // a..z + (ch >= 0x30 && ch <= 0x39) || // 0..9 + ch === 0x5c || // \ (backslash) + (ch >= 0x80 && + Regex.NonAsciiIdentifierPart.test(String.fromCharCode(ch))) + ); } // 7.6.1.2 Future Reserved Words function isFutureReservedWord(id) { - switch (id) { + switch (id) { case 'class': case 'enum': case 'export': case 'extends': case 'import': case 'super': - return true; + return true; default: - return false; - } + return false; + } } function isStrictModeReservedWord(id) { - switch (id) { + switch (id) { case 'implements': case 'interface': case 'package': @@ -291,2174 +386,2339 @@ parseStatement: true, parseSourceElement: true */ case 'static': case 'yield': case 'let': - return true; + return true; default: - return false; - } + return false; + } } function isRestrictedWord(id) { - return id === 'eval' || id === 'arguments'; + return id === 'eval' || id === 'arguments'; } // 7.6.1.1 Keywords function isKeyword(id) { - if (strict && isStrictModeReservedWord(id)) { - return true; - } + if (strict && isStrictModeReservedWord(id)) { + return true; + } - // 'const' is specialized as Keyword in V8. - // 'yield' and 'let' are for compatiblity with SpiderMonkey and ES.next. - // Some others are from future reserved words. + // 'const' is specialized as Keyword in V8. + // 'yield' and 'let' are for compatiblity with SpiderMonkey and ES.next. + // Some others are from future reserved words. - switch (id.length) { + switch (id.length) { case 2: - return (id === 'if') || (id === 'in') || (id === 'do'); + return id === 'if' || id === 'in' || id === 'do'; case 3: - return (id === 'var') || (id === 'for') || (id === 'new') || - (id === 'try') || (id === 'let'); + return ( + id === 'var' || + id === 'for' || + id === 'new' || + id === 'try' || + id === 'let' + ); case 4: - return (id === 'this') || (id === 'else') || (id === 'case') || - (id === 'void') || (id === 'with') || (id === 'enum'); + return ( + id === 'this' || + id === 'else' || + id === 'case' || + id === 'void' || + id === 'with' || + id === 'enum' + ); case 5: - return (id === 'while') || (id === 'break') || (id === 'catch') || - (id === 'throw') || (id === 'const') || (id === 'yield') || - (id === 'class') || (id === 'super'); + return ( + id === 'while' || + id === 'break' || + id === 'catch' || + id === 'throw' || + id === 'const' || + id === 'yield' || + id === 'class' || + id === 'super' + ); case 6: - return (id === 'return') || (id === 'typeof') || (id === 'delete') || - (id === 'switch') || (id === 'export') || (id === 'import'); + return ( + id === 'return' || + id === 'typeof' || + id === 'delete' || + id === 'switch' || + id === 'export' || + id === 'import' + ); case 7: - return (id === 'default') || (id === 'finally') || (id === 'extends'); + return id === 'default' || id === 'finally' || id === 'extends'; case 8: - return (id === 'function') || (id === 'continue') || (id === 'debugger'); + return id === 'function' || id === 'continue' || id === 'debugger'; case 10: - return (id === 'instanceof'); + return id === 'instanceof'; default: - return false; - } + return false; + } } // 7.4 Comments function addComment(type, value, start, end, loc) { - var comment, attacher; + var comment, attacher; - assert(typeof start === 'number', 'Comment must have valid position'); + assert(typeof start === 'number', 'Comment must have valid position'); - // Because the way the actual token is scanned, often the comments - // (if any) are skipped twice during the lexical analysis. - // Thus, we need to skip adding a comment if the comment array already - // handled it. - if (state.lastCommentStart >= start) { - return; - } - state.lastCommentStart = start; + // Because the way the actual token is scanned, often the comments + // (if any) are skipped twice during the lexical analysis. + // Thus, we need to skip adding a comment if the comment array already + // handled it. + if (state.lastCommentStart >= start) { + return; + } + state.lastCommentStart = start; - comment = { - type: type, - value: value - }; - if (extra.range) { - comment.range = [start, end]; - } - if (extra.loc) { - comment.loc = loc; - } - extra.comments.push(comment); - if (extra.attachComment) { - extra.leadingComments.push(comment); - extra.trailingComments.push(comment); - } + comment = { + type: type, + value: value, + }; + if (extra.range) { + comment.range = [start, end]; + } + if (extra.loc) { + comment.loc = loc; + } + extra.comments.push(comment); + if (extra.attachComment) { + extra.leadingComments.push(comment); + extra.trailingComments.push(comment); + } } function skipSingleLineComment(offset) { - var start, loc, ch, comment; + var start, loc, ch, comment; - start = index - offset; - loc = { - start: { - line: lineNumber, - column: index - lineStart - offset - } - }; + start = index - offset; + loc = { + start: { + line: lineNumber, + column: index - lineStart - offset, + }, + }; - while (index < length) { - ch = source.charCodeAt(index); - ++index; - if (isLineTerminator(ch)) { - if (extra.comments) { - comment = source.slice(start + offset, index - 1); - loc.end = { - line: lineNumber, - column: index - lineStart - 1 - }; - addComment('Line', comment, start, index - 1, loc); - } - if (ch === 13 && source.charCodeAt(index) === 10) { - ++index; - } - ++lineNumber; - lineStart = index; - return; - } - } - - if (extra.comments) { - comment = source.slice(start + offset, index); + while (index < length) { + ch = source.charCodeAt(index); + ++index; + if (isLineTerminator(ch)) { + if (extra.comments) { + comment = source.slice(start + offset, index - 1); loc.end = { - line: lineNumber, - column: index - lineStart + line: lineNumber, + column: index - lineStart - 1, }; - addComment('Line', comment, start, index, loc); + addComment('Line', comment, start, index - 1, loc); + } + if (ch === 13 && source.charCodeAt(index) === 10) { + ++index; + } + ++lineNumber; + lineStart = index; + return; } + } + + if (extra.comments) { + comment = source.slice(start + offset, index); + loc.end = { + line: lineNumber, + column: index - lineStart, + }; + addComment('Line', comment, start, index, loc); + } } function skipMultiLineComment() { - var start, loc, ch, comment; + var start, loc, ch, comment; - if (extra.comments) { - start = index - 2; - loc = { - start: { - line: lineNumber, - column: index - lineStart - 2 - } - }; - } + if (extra.comments) { + start = index - 2; + loc = { + start: { + line: lineNumber, + column: index - lineStart - 2, + }, + }; + } - while (index < length) { - ch = source.charCodeAt(index); - if (isLineTerminator(ch)) { - if (ch === 0x0D && source.charCodeAt(index + 1) === 0x0A) { - ++index; - } - ++lineNumber; - ++index; - lineStart = index; - if (index >= length) { - throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); - } - } else if (ch === 0x2A) { - // Block comment ends with '*/'. - if (source.charCodeAt(index + 1) === 0x2F) { - ++index; - ++index; - if (extra.comments) { - comment = source.slice(start + 2, index - 2); - loc.end = { - line: lineNumber, - column: index - lineStart - }; - addComment('Block', comment, start, index, loc); - } - return; - } - ++index; - } else { - ++index; + while (index < length) { + ch = source.charCodeAt(index); + if (isLineTerminator(ch)) { + if (ch === 0x0d && source.charCodeAt(index + 1) === 0x0a) { + ++index; + } + ++lineNumber; + ++index; + lineStart = index; + if (index >= length) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } else if (ch === 0x2a) { + // Block comment ends with '*/'. + if (source.charCodeAt(index + 1) === 0x2f) { + ++index; + ++index; + if (extra.comments) { + comment = source.slice(start + 2, index - 2); + loc.end = { + line: lineNumber, + column: index - lineStart, + }; + addComment('Block', comment, start, index, loc); } + return; + } + ++index; + } else { + ++index; } + } - throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); } function skipComment() { - var ch, start; + var ch, start; - start = (index === 0); - while (index < length) { - ch = source.charCodeAt(index); + start = index === 0; + while (index < length) { + ch = source.charCodeAt(index); - if (isWhiteSpace(ch)) { - ++index; - } else if (isLineTerminator(ch)) { - ++index; - if (ch === 0x0D && source.charCodeAt(index) === 0x0A) { - ++index; - } - ++lineNumber; - lineStart = index; - start = true; - } else if (ch === 0x2F) { // U+002F is '/' - ch = source.charCodeAt(index + 1); - if (ch === 0x2F) { - ++index; - ++index; - skipSingleLineComment(2); - start = true; - } else if (ch === 0x2A) { // U+002A is '*' - ++index; - ++index; - skipMultiLineComment(); - } else { - break; - } - } else if (start && ch === 0x2D) { // U+002D is '-' - // U+003E is '>' - if ((source.charCodeAt(index + 1) === 0x2D) && (source.charCodeAt(index + 2) === 0x3E)) { - // '-->' is a single-line comment - index += 3; - skipSingleLineComment(3); - } else { - break; - } - } else if (ch === 0x3C) { // U+003C is '<' - if (source.slice(index + 1, index + 4) === '!--') { - ++index; // `<` - ++index; // `!` - ++index; // `-` - ++index; // `-` - skipSingleLineComment(4); - } else { - break; - } - } else { - break; - } + if (isWhiteSpace(ch)) { + ++index; + } else if (isLineTerminator(ch)) { + ++index; + if (ch === 0x0d && source.charCodeAt(index) === 0x0a) { + ++index; + } + ++lineNumber; + lineStart = index; + start = true; + } else if (ch === 0x2f) { + // U+002F is '/' + ch = source.charCodeAt(index + 1); + if (ch === 0x2f) { + ++index; + ++index; + skipSingleLineComment(2); + start = true; + } else if (ch === 0x2a) { + // U+002A is '*' + ++index; + ++index; + skipMultiLineComment(); + } else { + break; + } + } else if (start && ch === 0x2d) { + // U+002D is '-' + // U+003E is '>' + if ( + source.charCodeAt(index + 1) === 0x2d && + source.charCodeAt(index + 2) === 0x3e + ) { + // '-->' is a single-line comment + index += 3; + skipSingleLineComment(3); + } else { + break; + } + } else if (ch === 0x3c) { + // U+003C is '<' + if (source.slice(index + 1, index + 4) === '!--') { + ++index; // `<` + ++index; // `!` + ++index; // `-` + ++index; // `-` + skipSingleLineComment(4); + } else { + break; + } + } else { + break; } + } } function scanHexEscape(prefix) { - var i, len, ch, code = 0; + var i, + len, + ch, + code = 0; - len = (prefix === 'u') ? 4 : 2; - for (i = 0; i < len; ++i) { - if (index < length && isHexDigit(source[index])) { - ch = source[index++]; - code = code * 16 + '0123456789abcdef'.indexOf(ch.toLowerCase()); - } else { - return ''; - } + len = prefix === 'u' ? 4 : 2; + for (i = 0; i < len; ++i) { + if (index < length && isHexDigit(source[index])) { + ch = source[index++]; + code = code * 16 + '0123456789abcdef'.indexOf(ch.toLowerCase()); + } else { + return ''; } - return String.fromCharCode(code); + } + return String.fromCharCode(code); } function getEscapedIdentifier() { - var ch, id; + var ch, id; - ch = source.charCodeAt(index++); - id = String.fromCharCode(ch); + ch = source.charCodeAt(index++); + id = String.fromCharCode(ch); + + // '\u' (U+005C, U+0075) denotes an escaped character. + if (ch === 0x5c) { + if (source.charCodeAt(index) !== 0x75) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + ++index; + ch = scanHexEscape('u'); + if (!ch || ch === '\\' || !isIdentifierStart(ch.charCodeAt(0))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + id = ch; + } + + while (index < length) { + ch = source.charCodeAt(index); + if (!isIdentifierPart(ch)) { + break; + } + ++index; + id += String.fromCharCode(ch); // '\u' (U+005C, U+0075) denotes an escaped character. - if (ch === 0x5C) { - if (source.charCodeAt(index) !== 0x75) { - throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); - } - ++index; - ch = scanHexEscape('u'); - if (!ch || ch === '\\' || !isIdentifierStart(ch.charCodeAt(0))) { - throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); - } - id = ch; + if (ch === 0x5c) { + id = id.substr(0, id.length - 1); + if (source.charCodeAt(index) !== 0x75) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + ++index; + ch = scanHexEscape('u'); + if (!ch || ch === '\\' || !isIdentifierPart(ch.charCodeAt(0))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + id += ch; } + } - while (index < length) { - ch = source.charCodeAt(index); - if (!isIdentifierPart(ch)) { - break; - } - ++index; - id += String.fromCharCode(ch); - - // '\u' (U+005C, U+0075) denotes an escaped character. - if (ch === 0x5C) { - id = id.substr(0, id.length - 1); - if (source.charCodeAt(index) !== 0x75) { - throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); - } - ++index; - ch = scanHexEscape('u'); - if (!ch || ch === '\\' || !isIdentifierPart(ch.charCodeAt(0))) { - throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); - } - id += ch; - } - } - - return id; + return id; } function getIdentifier() { - var start, ch; + var start, ch; - start = index++; - while (index < length) { - ch = source.charCodeAt(index); - if (ch === 0x5C) { - // Blackslash (U+005C) marks Unicode escape sequence. - index = start; - return getEscapedIdentifier(); - } - if (isIdentifierPart(ch)) { - ++index; - } else { - break; - } + start = index++; + while (index < length) { + ch = source.charCodeAt(index); + if (ch === 0x5c) { + // Blackslash (U+005C) marks Unicode escape sequence. + index = start; + return getEscapedIdentifier(); } + if (isIdentifierPart(ch)) { + ++index; + } else { + break; + } + } - return source.slice(start, index); + return source.slice(start, index); } function scanIdentifier() { - var start, id, type; + var start, id, type; - start = index; + start = index; - // Backslash (U+005C) starts an escaped character. - id = (source.charCodeAt(index) === 0x5C) ? getEscapedIdentifier() : getIdentifier(); + // Backslash (U+005C) starts an escaped character. + id = + source.charCodeAt(index) === 0x5c + ? getEscapedIdentifier() + : getIdentifier(); - // There is no keyword or literal with only one character. - // Thus, it must be an identifier. - if (id.length === 1) { - type = Token.Identifier; - } else if (isKeyword(id)) { - type = Token.Keyword; - } else if (id === 'null') { - type = Token.NullLiteral; - } else if (id === 'true' || id === 'false') { - type = Token.BooleanLiteral; - } else { - type = Token.Identifier; - } + // There is no keyword or literal with only one character. + // Thus, it must be an identifier. + if (id.length === 1) { + type = Token.Identifier; + } else if (isKeyword(id)) { + type = Token.Keyword; + } else if (id === 'null') { + type = Token.NullLiteral; + } else if (id === 'true' || id === 'false') { + type = Token.BooleanLiteral; + } else { + type = Token.Identifier; + } - return { - type: type, - value: id, - lineNumber: lineNumber, - lineStart: lineStart, - start: start, - end: index - }; + return { + type: type, + value: id, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index, + }; } - // 7.7 Punctuators function scanPunctuator() { - var start = index, - code = source.charCodeAt(index), - code2, - ch1 = source[index], - ch2, - ch3, - ch4; - - switch (code) { + var start = index, + code = source.charCodeAt(index), + code2, + ch1 = source[index], + ch2, + ch3, + ch4; + switch (code) { // Check for most common single-character punctuators. - case 0x2E: // . dot - case 0x28: // ( open bracket - case 0x29: // ) close bracket - case 0x3B: // ; semicolon - case 0x2C: // , comma - case 0x7B: // { open curly brace - case 0x7D: // } close curly brace - case 0x5B: // [ - case 0x5D: // ] - case 0x3A: // : - case 0x3F: // ? - case 0x7E: // ~ - ++index; - if (extra.tokenize) { - if (code === 0x28) { - extra.openParenToken = extra.tokens.length; - } else if (code === 0x7B) { - extra.openCurlyToken = extra.tokens.length; - } + case 0x2e: // . dot + case 0x28: // ( open bracket + case 0x29: // ) close bracket + case 0x3b: // ; semicolon + case 0x2c: // , comma + case 0x7b: // { open curly brace + case 0x7d: // } close curly brace + case 0x5b: // [ + case 0x5d: // ] + case 0x3a: // : + case 0x3f: // ? + case 0x7e: // ~ + ++index; + if (extra.tokenize) { + if (code === 0x28) { + extra.openParenToken = extra.tokens.length; + } else if (code === 0x7b) { + extra.openCurlyToken = extra.tokens.length; } - return { - type: Token.Punctuator, - value: String.fromCharCode(code), - lineNumber: lineNumber, - lineStart: lineStart, - start: start, - end: index - }; + } + return { + type: Token.Punctuator, + value: String.fromCharCode(code), + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index, + }; default: - code2 = source.charCodeAt(index + 1); + code2 = source.charCodeAt(index + 1); - // '=' (U+003D) marks an assignment or comparison operator. - if (code2 === 0x3D) { - switch (code) { - case 0x2B: // + - case 0x2D: // - - case 0x2F: // / - case 0x3C: // < - case 0x3E: // > - case 0x5E: // ^ - case 0x7C: // | - case 0x25: // % - case 0x26: // & - case 0x2A: // * - index += 2; - return { - type: Token.Punctuator, - value: String.fromCharCode(code) + String.fromCharCode(code2), - lineNumber: lineNumber, - lineStart: lineStart, - start: start, - end: index - }; + // '=' (U+003D) marks an assignment or comparison operator. + if (code2 === 0x3d) { + switch (code) { + case 0x2b: // + + case 0x2d: // - + case 0x2f: // / + case 0x3c: // < + case 0x3e: // > + case 0x5e: // ^ + case 0x7c: // | + case 0x25: // % + case 0x26: // & + case 0x2a: // * + index += 2; + return { + type: Token.Punctuator, + value: String.fromCharCode(code) + String.fromCharCode(code2), + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index, + }; - case 0x21: // ! - case 0x3D: // = - index += 2; + case 0x21: // ! + case 0x3d: // = + index += 2; - // !== and === - if (source.charCodeAt(index) === 0x3D) { - ++index; - } - return { - type: Token.Punctuator, - value: source.slice(start, index), - lineNumber: lineNumber, - lineStart: lineStart, - start: start, - end: index - }; + // !== and === + if (source.charCodeAt(index) === 0x3d) { + ++index; } + return { + type: Token.Punctuator, + value: source.slice(start, index), + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index, + }; } - } + } + } - // 4-character punctuator: >>>= + // 4-character punctuator: >>>= - ch4 = source.substr(index, 4); + ch4 = source.substr(index, 4); - if (ch4 === '>>>=') { - index += 4; - return { - type: Token.Punctuator, - value: ch4, - lineNumber: lineNumber, - lineStart: lineStart, - start: start, - end: index - }; - } + if (ch4 === '>>>=') { + index += 4; + return { + type: Token.Punctuator, + value: ch4, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index, + }; + } - // 3-character punctuators: === !== >>> <<= >>= + // 3-character punctuators: === !== >>> <<= >>= - ch3 = ch4.substr(0, 3); + ch3 = ch4.substr(0, 3); - if (ch3 === '>>>' || ch3 === '<<=' || ch3 === '>>=') { - index += 3; - return { - type: Token.Punctuator, - value: ch3, - lineNumber: lineNumber, - lineStart: lineStart, - start: start, - end: index - }; - } + if (ch3 === '>>>' || ch3 === '<<=' || ch3 === '>>=') { + index += 3; + return { + type: Token.Punctuator, + value: ch3, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index, + }; + } - // Other 2-character punctuators: ++ -- << >> && || - ch2 = ch3.substr(0, 2); + // Other 2-character punctuators: ++ -- << >> && || + ch2 = ch3.substr(0, 2); - if ((ch1 === ch2[1] && ('+-<>&|'.indexOf(ch1) >= 0)) || ch2 === '=>') { - index += 2; - return { - type: Token.Punctuator, - value: ch2, - lineNumber: lineNumber, - lineStart: lineStart, - start: start, - end: index - }; - } + if ((ch1 === ch2[1] && '+-<>&|'.indexOf(ch1) >= 0) || ch2 === '=>') { + index += 2; + return { + type: Token.Punctuator, + value: ch2, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index, + }; + } - // 1-character punctuators: < > = ! + - * % & | ^ / - if ('<>=!+-*%&|^/'.indexOf(ch1) >= 0) { - ++index; - return { - type: Token.Punctuator, - value: ch1, - lineNumber: lineNumber, - lineStart: lineStart, - start: start, - end: index - }; - } + // 1-character punctuators: < > = ! + - * % & | ^ / + if ('<>=!+-*%&|^/'.indexOf(ch1) >= 0) { + ++index; + return { + type: Token.Punctuator, + value: ch1, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index, + }; + } - throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); } // 7.8.3 Numeric Literals function scanHexLiteral(start) { - var number = ''; + var number = ''; - while (index < length) { - if (!isHexDigit(source[index])) { - break; - } - number += source[index++]; + while (index < length) { + if (!isHexDigit(source[index])) { + break; } + number += source[index++]; + } - if (number.length === 0) { - throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); - } + if (number.length === 0) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } - if (isIdentifierStart(source.charCodeAt(index))) { - throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); - } + if (isIdentifierStart(source.charCodeAt(index))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } - return { - type: Token.NumericLiteral, - value: parseInt('0x' + number, 16), - lineNumber: lineNumber, - lineStart: lineStart, - start: start, - end: index - }; + return { + type: Token.NumericLiteral, + value: parseInt('0x' + number, 16), + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index, + }; } function scanOctalLiteral(start) { - var number = '0' + source[index++]; - while (index < length) { - if (!isOctalDigit(source[index])) { - break; - } - number += source[index++]; + var number = '0' + source[index++]; + while (index < length) { + if (!isOctalDigit(source[index])) { + break; } + number += source[index++]; + } - if (isIdentifierStart(source.charCodeAt(index)) || isDecimalDigit(source.charCodeAt(index))) { - throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); - } + if ( + isIdentifierStart(source.charCodeAt(index)) || + isDecimalDigit(source.charCodeAt(index)) + ) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } - return { - type: Token.NumericLiteral, - value: parseInt(number, 8), - octal: true, - lineNumber: lineNumber, - lineStart: lineStart, - start: start, - end: index - }; + return { + type: Token.NumericLiteral, + value: parseInt(number, 8), + octal: true, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index, + }; } function scanNumericLiteral() { - var number, start, ch; + var number, start, ch; + + ch = source[index]; + assert( + isDecimalDigit(ch.charCodeAt(0)) || ch === '.', + 'Numeric literal must start with a decimal digit or a decimal point' + ); + + start = index; + number = ''; + if (ch !== '.') { + number = source[index++]; + ch = source[index]; + + // Hex number starts with '0x'. + // Octal number starts with '0'. + if (number === '0') { + if (ch === 'x' || ch === 'X') { + ++index; + return scanHexLiteral(start); + } + if (isOctalDigit(ch)) { + return scanOctalLiteral(start); + } + + // decimal number starts with '0' such as '09' is illegal. + if (ch && isDecimalDigit(ch.charCodeAt(0))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } + + while (isDecimalDigit(source.charCodeAt(index))) { + number += source[index++]; + } + ch = source[index]; + } + + if (ch === '.') { + number += source[index++]; + while (isDecimalDigit(source.charCodeAt(index))) { + number += source[index++]; + } + ch = source[index]; + } + + if (ch === 'e' || ch === 'E') { + number += source[index++]; ch = source[index]; - assert(isDecimalDigit(ch.charCodeAt(0)) || (ch === '.'), - 'Numeric literal must start with a decimal digit or a decimal point'); - - start = index; - number = ''; - if (ch !== '.') { - number = source[index++]; - ch = source[index]; - - // Hex number starts with '0x'. - // Octal number starts with '0'. - if (number === '0') { - if (ch === 'x' || ch === 'X') { - ++index; - return scanHexLiteral(start); - } - if (isOctalDigit(ch)) { - return scanOctalLiteral(start); - } - - // decimal number starts with '0' such as '09' is illegal. - if (ch && isDecimalDigit(ch.charCodeAt(0))) { - throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); - } - } - - while (isDecimalDigit(source.charCodeAt(index))) { - number += source[index++]; - } - ch = source[index]; + if (ch === '+' || ch === '-') { + number += source[index++]; } - - if (ch === '.') { + if (isDecimalDigit(source.charCodeAt(index))) { + while (isDecimalDigit(source.charCodeAt(index))) { number += source[index++]; - while (isDecimalDigit(source.charCodeAt(index))) { - number += source[index++]; - } - ch = source[index]; + } + } else { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); } + } - if (ch === 'e' || ch === 'E') { - number += source[index++]; + if (isIdentifierStart(source.charCodeAt(index))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } - ch = source[index]; - if (ch === '+' || ch === '-') { - number += source[index++]; - } - if (isDecimalDigit(source.charCodeAt(index))) { - while (isDecimalDigit(source.charCodeAt(index))) { - number += source[index++]; - } - } else { - throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); - } - } - - if (isIdentifierStart(source.charCodeAt(index))) { - throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); - } - - return { - type: Token.NumericLiteral, - value: parseFloat(number), - lineNumber: lineNumber, - lineStart: lineStart, - start: start, - end: index - }; + return { + type: Token.NumericLiteral, + value: parseFloat(number), + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index, + }; } // 7.8.4 String Literals function scanStringLiteral() { - var str = '', quote, start, ch, code, unescaped, restore, octal = false, startLineNumber, startLineStart; - startLineNumber = lineNumber; - startLineStart = lineStart; + var str = '', + quote, + start, + ch, + code, + unescaped, + restore, + octal = false, + startLineNumber, + startLineStart; + startLineNumber = lineNumber; + startLineStart = lineStart; - quote = source[index]; - assert((quote === '\'' || quote === '"'), - 'String literal must starts with a quote'); + quote = source[index]; + assert( + quote === "'" || quote === '"', + 'String literal must starts with a quote' + ); - start = index; - ++index; + start = index; + ++index; - while (index < length) { - ch = source[index++]; + while (index < length) { + ch = source[index++]; - if (ch === quote) { - quote = ''; - break; - } else if (ch === '\\') { - ch = source[index++]; - if (!ch || !isLineTerminator(ch.charCodeAt(0))) { - switch (ch) { - case 'u': - case 'x': - restore = index; - unescaped = scanHexEscape(ch); - if (unescaped) { - str += unescaped; - } else { - index = restore; - str += ch; - } - break; - case 'n': - str += '\n'; - break; - case 'r': - str += '\r'; - break; - case 't': - str += '\t'; - break; - case 'b': - str += '\b'; - break; - case 'f': - str += '\f'; - break; - case 'v': - str += '\x0B'; - break; - - default: - if (isOctalDigit(ch)) { - code = '01234567'.indexOf(ch); - - // \0 is not octal escape sequence - if (code !== 0) { - octal = true; - } - - if (index < length && isOctalDigit(source[index])) { - octal = true; - code = code * 8 + '01234567'.indexOf(source[index++]); - - // 3 digits are only allowed when string starts - // with 0, 1, 2, 3 - if ('0123'.indexOf(ch) >= 0 && - index < length && - isOctalDigit(source[index])) { - code = code * 8 + '01234567'.indexOf(source[index++]); - } - } - str += String.fromCharCode(code); - } else { - str += ch; - } - break; - } + if (ch === quote) { + quote = ''; + break; + } else if (ch === '\\') { + ch = source[index++]; + if (!ch || !isLineTerminator(ch.charCodeAt(0))) { + switch (ch) { + case 'u': + case 'x': + restore = index; + unescaped = scanHexEscape(ch); + if (unescaped) { + str += unescaped; } else { - ++lineNumber; - if (ch === '\r' && source[index] === '\n') { - ++index; - } - lineStart = index; + index = restore; + str += ch; + } + break; + case 'n': + str += '\n'; + break; + case 'r': + str += '\r'; + break; + case 't': + str += '\t'; + break; + case 'b': + str += '\b'; + break; + case 'f': + str += '\f'; + break; + case 'v': + str += '\x0B'; + break; + + default: + if (isOctalDigit(ch)) { + code = '01234567'.indexOf(ch); + + // \0 is not octal escape sequence + if (code !== 0) { + octal = true; + } + + if (index < length && isOctalDigit(source[index])) { + octal = true; + code = code * 8 + '01234567'.indexOf(source[index++]); + + // 3 digits are only allowed when string starts + // with 0, 1, 2, 3 + if ( + '0123'.indexOf(ch) >= 0 && + index < length && + isOctalDigit(source[index]) + ) { + code = code * 8 + '01234567'.indexOf(source[index++]); + } + } + str += String.fromCharCode(code); + } else { + str += ch; } - } else if (isLineTerminator(ch.charCodeAt(0))) { break; - } else { - str += ch; } + } else { + ++lineNumber; + if (ch === '\r' && source[index] === '\n') { + ++index; + } + lineStart = index; + } + } else if (isLineTerminator(ch.charCodeAt(0))) { + break; + } else { + str += ch; } + } - if (quote !== '') { - throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); - } + if (quote !== '') { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } - return { - type: Token.StringLiteral, - value: str, - octal: octal, - startLineNumber: startLineNumber, - startLineStart: startLineStart, - lineNumber: lineNumber, - lineStart: lineStart, - start: start, - end: index - }; + return { + type: Token.StringLiteral, + value: str, + octal: octal, + startLineNumber: startLineNumber, + startLineStart: startLineStart, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index, + }; } function testRegExp(pattern, flags) { - var value; - try { - value = new RegExp(pattern, flags); - } catch (e) { - throwError({}, Messages.InvalidRegExp); - } - return value; + var value; + try { + value = new RegExp(pattern, flags); + } catch (e) { + throwError({}, Messages.InvalidRegExp); + } + return value; } function scanRegExpBody() { - var ch, str, classMarker, terminated, body; + var ch, str, classMarker, terminated, body; - ch = source[index]; - assert(ch === '/', 'Regular expression literal must start with a slash'); - str = source[index++]; + ch = source[index]; + assert(ch === '/', 'Regular expression literal must start with a slash'); + str = source[index++]; - classMarker = false; - terminated = false; - while (index < length) { - ch = source[index++]; - str += ch; - if (ch === '\\') { - ch = source[index++]; - // ECMA-262 7.8.5 - if (isLineTerminator(ch.charCodeAt(0))) { - throwError({}, Messages.UnterminatedRegExp); - } - str += ch; - } else if (isLineTerminator(ch.charCodeAt(0))) { - throwError({}, Messages.UnterminatedRegExp); - } else if (classMarker) { - if (ch === ']') { - classMarker = false; - } - } else { - if (ch === '/') { - terminated = true; - break; - } else if (ch === '[') { - classMarker = true; - } - } - } - - if (!terminated) { + classMarker = false; + terminated = false; + while (index < length) { + ch = source[index++]; + str += ch; + if (ch === '\\') { + ch = source[index++]; + // ECMA-262 7.8.5 + if (isLineTerminator(ch.charCodeAt(0))) { throwError({}, Messages.UnterminatedRegExp); + } + str += ch; + } else if (isLineTerminator(ch.charCodeAt(0))) { + throwError({}, Messages.UnterminatedRegExp); + } else if (classMarker) { + if (ch === ']') { + classMarker = false; + } + } else { + if (ch === '/') { + terminated = true; + break; + } else if (ch === '[') { + classMarker = true; + } } + } - // Exclude leading and trailing slash. - body = str.substr(1, str.length - 2); - return { - value: body, - literal: str - }; + if (!terminated) { + throwError({}, Messages.UnterminatedRegExp); + } + + // Exclude leading and trailing slash. + body = str.substr(1, str.length - 2); + return { + value: body, + literal: str, + }; } function scanRegExpFlags() { - var ch, str, flags, restore; + var ch, str, flags, restore; - str = ''; - flags = ''; - while (index < length) { - ch = source[index]; - if (!isIdentifierPart(ch.charCodeAt(0))) { - break; - } - - ++index; - if (ch === '\\' && index < length) { - ch = source[index]; - if (ch === 'u') { - ++index; - restore = index; - ch = scanHexEscape('u'); - if (ch) { - flags += ch; - for (str += '\\u'; restore < index; ++restore) { - str += source[restore]; - } - } else { - index = restore; - flags += 'u'; - str += '\\u'; - } - throwErrorTolerant({}, Messages.UnexpectedToken, 'ILLEGAL'); - } else { - str += '\\'; - throwErrorTolerant({}, Messages.UnexpectedToken, 'ILLEGAL'); - } - } else { - flags += ch; - str += ch; - } + str = ''; + flags = ''; + while (index < length) { + ch = source[index]; + if (!isIdentifierPart(ch.charCodeAt(0))) { + break; } - return { - value: flags, - literal: str - }; + ++index; + if (ch === '\\' && index < length) { + ch = source[index]; + if (ch === 'u') { + ++index; + restore = index; + ch = scanHexEscape('u'); + if (ch) { + flags += ch; + for (str += '\\u'; restore < index; ++restore) { + str += source[restore]; + } + } else { + index = restore; + flags += 'u'; + str += '\\u'; + } + throwErrorTolerant({}, Messages.UnexpectedToken, 'ILLEGAL'); + } else { + str += '\\'; + throwErrorTolerant({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } else { + flags += ch; + str += ch; + } + } + + return { + value: flags, + literal: str, + }; } function scanRegExp() { - var start, body, flags, pattern, value; + var start, body, flags, pattern, value; - lookahead = null; - skipComment(); - start = index; + lookahead = null; + skipComment(); + start = index; - body = scanRegExpBody(); - flags = scanRegExpFlags(); - value = testRegExp(body.value, flags.value); - - if (extra.tokenize) { - return { - type: Token.RegularExpression, - value: value, - lineNumber: lineNumber, - lineStart: lineStart, - start: start, - end: index - }; - } + body = scanRegExpBody(); + flags = scanRegExpFlags(); + value = testRegExp(body.value, flags.value); + if (extra.tokenize) { return { - literal: body.literal + flags.literal, - value: value, - start: start, - end: index + type: Token.RegularExpression, + value: value, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index, }; + } + + return { + literal: body.literal + flags.literal, + value: value, + start: start, + end: index, + }; } function collectRegex() { - var pos, loc, regex, token; + var pos, loc, regex, token; - skipComment(); + skipComment(); - pos = index; - loc = { - start: { - line: lineNumber, - column: index - lineStart + pos = index; + loc = { + start: { + line: lineNumber, + column: index - lineStart, + }, + }; + + regex = scanRegExp(); + loc.end = { + line: lineNumber, + column: index - lineStart, + }; + + /* istanbul ignore next */ + if (!extra.tokenize) { + // Pop the previous token, which is likely '/' or '/=' + if (extra.tokens.length > 0) { + token = extra.tokens[extra.tokens.length - 1]; + if (token.range[0] === pos && token.type === 'Punctuator') { + if (token.value === '/' || token.value === '/=') { + extra.tokens.pop(); } - }; - - regex = scanRegExp(); - loc.end = { - line: lineNumber, - column: index - lineStart - }; - - /* istanbul ignore next */ - if (!extra.tokenize) { - // Pop the previous token, which is likely '/' or '/=' - if (extra.tokens.length > 0) { - token = extra.tokens[extra.tokens.length - 1]; - if (token.range[0] === pos && token.type === 'Punctuator') { - if (token.value === '/' || token.value === '/=') { - extra.tokens.pop(); - } - } - } - - extra.tokens.push({ - type: 'RegularExpression', - value: regex.literal, - range: [pos, index], - loc: loc - }); + } } - return regex; + extra.tokens.push({ + type: 'RegularExpression', + value: regex.literal, + range: [pos, index], + loc: loc, + }); + } + + return regex; } function isIdentifierName(token) { - return token.type === Token.Identifier || - token.type === Token.Keyword || - token.type === Token.BooleanLiteral || - token.type === Token.NullLiteral; + return ( + token.type === Token.Identifier || + token.type === Token.Keyword || + token.type === Token.BooleanLiteral || + token.type === Token.NullLiteral + ); } function advanceSlash() { - var prevToken, - checkToken; - // Using the following algorithm: - // https://github.com/mozilla/sweet.js/wiki/design - prevToken = extra.tokens[extra.tokens.length - 1]; - if (!prevToken) { - // Nothing before that: it cannot be a division. - return collectRegex(); + var prevToken, checkToken; + // Using the following algorithm: + // https://github.com/mozilla/sweet.js/wiki/design + prevToken = extra.tokens[extra.tokens.length - 1]; + if (!prevToken) { + // Nothing before that: it cannot be a division. + return collectRegex(); + } + if (prevToken.type === 'Punctuator') { + if (prevToken.value === ']') { + return scanPunctuator(); } - if (prevToken.type === 'Punctuator') { - if (prevToken.value === ']') { - return scanPunctuator(); - } - if (prevToken.value === ')') { - checkToken = extra.tokens[extra.openParenToken - 1]; - if (checkToken && - checkToken.type === 'Keyword' && - (checkToken.value === 'if' || - checkToken.value === 'while' || - checkToken.value === 'for' || - checkToken.value === 'with')) { - return collectRegex(); - } - return scanPunctuator(); - } - if (prevToken.value === '}') { - // Dividing a function by anything makes little sense, - // but we have to check for that. - if (extra.tokens[extra.openCurlyToken - 3] && - extra.tokens[extra.openCurlyToken - 3].type === 'Keyword') { - // Anonymous function. - checkToken = extra.tokens[extra.openCurlyToken - 4]; - if (!checkToken) { - return scanPunctuator(); - } - } else if (extra.tokens[extra.openCurlyToken - 4] && - extra.tokens[extra.openCurlyToken - 4].type === 'Keyword') { - // Named function. - checkToken = extra.tokens[extra.openCurlyToken - 5]; - if (!checkToken) { - return collectRegex(); - } - } else { - return scanPunctuator(); - } - // checkToken determines whether the function is - // a declaration or an expression. - if (FnExprTokens.indexOf(checkToken.value) >= 0) { - // It is an expression. - return scanPunctuator(); - } - // It is a declaration. - return collectRegex(); - } + if (prevToken.value === ')') { + checkToken = extra.tokens[extra.openParenToken - 1]; + if ( + checkToken && + checkToken.type === 'Keyword' && + (checkToken.value === 'if' || + checkToken.value === 'while' || + checkToken.value === 'for' || + checkToken.value === 'with') + ) { return collectRegex(); + } + return scanPunctuator(); } - if (prevToken.type === 'Keyword') { - return collectRegex(); + if (prevToken.value === '}') { + // Dividing a function by anything makes little sense, + // but we have to check for that. + if ( + extra.tokens[extra.openCurlyToken - 3] && + extra.tokens[extra.openCurlyToken - 3].type === 'Keyword' + ) { + // Anonymous function. + checkToken = extra.tokens[extra.openCurlyToken - 4]; + if (!checkToken) { + return scanPunctuator(); + } + } else if ( + extra.tokens[extra.openCurlyToken - 4] && + extra.tokens[extra.openCurlyToken - 4].type === 'Keyword' + ) { + // Named function. + checkToken = extra.tokens[extra.openCurlyToken - 5]; + if (!checkToken) { + return collectRegex(); + } + } else { + return scanPunctuator(); + } + // checkToken determines whether the function is + // a declaration or an expression. + if (FnExprTokens.indexOf(checkToken.value) >= 0) { + // It is an expression. + return scanPunctuator(); + } + // It is a declaration. + return collectRegex(); } - return scanPunctuator(); + return collectRegex(); + } + if (prevToken.type === 'Keyword') { + return collectRegex(); + } + return scanPunctuator(); } function advance() { - var ch; + var ch; - skipComment(); + skipComment(); - if (index >= length) { - return { - type: Token.EOF, - lineNumber: lineNumber, - lineStart: lineStart, - start: index, - end: index - }; - } + if (index >= length) { + return { + type: Token.EOF, + lineNumber: lineNumber, + lineStart: lineStart, + start: index, + end: index, + }; + } - ch = source.charCodeAt(index); + ch = source.charCodeAt(index); - if (isIdentifierStart(ch)) { - return scanIdentifier(); - } - - // Very common: ( and ) and ; - if (ch === 0x28 || ch === 0x29 || ch === 0x3B) { - return scanPunctuator(); - } - - // String literal starts with single quote (U+0027) or double quote (U+0022). - if (ch === 0x27 || ch === 0x22) { - return scanStringLiteral(); - } - - - // Dot (.) U+002E can also start a floating-point number, hence the need - // to check the next character. - if (ch === 0x2E) { - if (isDecimalDigit(source.charCodeAt(index + 1))) { - return scanNumericLiteral(); - } - return scanPunctuator(); - } - - if (isDecimalDigit(ch)) { - return scanNumericLiteral(); - } - - // Slash (/) U+002F can also start a regex. - if (extra.tokenize && ch === 0x2F) { - return advanceSlash(); - } + if (isIdentifierStart(ch)) { + return scanIdentifier(); + } + // Very common: ( and ) and ; + if (ch === 0x28 || ch === 0x29 || ch === 0x3b) { return scanPunctuator(); + } + + // String literal starts with single quote (U+0027) or double quote (U+0022). + if (ch === 0x27 || ch === 0x22) { + return scanStringLiteral(); + } + + // Dot (.) U+002E can also start a floating-point number, hence the need + // to check the next character. + if (ch === 0x2e) { + if (isDecimalDigit(source.charCodeAt(index + 1))) { + return scanNumericLiteral(); + } + return scanPunctuator(); + } + + if (isDecimalDigit(ch)) { + return scanNumericLiteral(); + } + + // Slash (/) U+002F can also start a regex. + if (extra.tokenize && ch === 0x2f) { + return advanceSlash(); + } + + return scanPunctuator(); } function collectToken() { - var loc, token, range, value; + var loc, token, range, value; - skipComment(); - loc = { - start: { - line: lineNumber, - column: index - lineStart - } - }; + skipComment(); + loc = { + start: { + line: lineNumber, + column: index - lineStart, + }, + }; - token = advance(); - loc.end = { - line: lineNumber, - column: index - lineStart - }; + token = advance(); + loc.end = { + line: lineNumber, + column: index - lineStart, + }; - if (token.type !== Token.EOF) { - value = source.slice(token.start, token.end); - extra.tokens.push({ - type: TokenName[token.type], - value: value, - range: [token.start, token.end], - loc: loc - }); - } + if (token.type !== Token.EOF) { + value = source.slice(token.start, token.end); + extra.tokens.push({ + type: TokenName[token.type], + value: value, + range: [token.start, token.end], + loc: loc, + }); + } - return token; + return token; } function lex() { - var token; + var token; - token = lookahead; - index = token.end; - lineNumber = token.lineNumber; - lineStart = token.lineStart; + token = lookahead; + index = token.end; + lineNumber = token.lineNumber; + lineStart = token.lineStart; - lookahead = (typeof extra.tokens !== 'undefined') ? collectToken() : advance(); + lookahead = + typeof extra.tokens !== 'undefined' ? collectToken() : advance(); - index = token.end; - lineNumber = token.lineNumber; - lineStart = token.lineStart; + index = token.end; + lineNumber = token.lineNumber; + lineStart = token.lineStart; - return token; + return token; } function peek() { - var pos, line, start; + var pos, line, start; - pos = index; - line = lineNumber; - start = lineStart; - lookahead = (typeof extra.tokens !== 'undefined') ? collectToken() : advance(); - index = pos; - lineNumber = line; - lineStart = start; + pos = index; + line = lineNumber; + start = lineStart; + lookahead = + typeof extra.tokens !== 'undefined' ? collectToken() : advance(); + index = pos; + lineNumber = line; + lineStart = start; } function Position(line, column) { - this.line = line; - this.column = column; + this.line = line; + this.column = column; } function SourceLocation(startLine, startColumn, line, column) { - this.start = new Position(startLine, startColumn); - this.end = new Position(line, column); + this.start = new Position(startLine, startColumn); + this.end = new Position(line, column); } SyntaxTreeDelegate = { + name: 'SyntaxTree', - name: 'SyntaxTree', + processComment: function(node) { + var lastChild, trailingComments; - processComment: function (node) { - var lastChild, trailingComments; - - if (node.type === Syntax.Program) { - if (node.body.length > 0) { - return; - } - } - - if (extra.trailingComments.length > 0) { - if (extra.trailingComments[0].range[0] >= node.range[1]) { - trailingComments = extra.trailingComments; - extra.trailingComments = []; - } else { - extra.trailingComments.length = 0; - } - } else { - if (extra.bottomRightStack.length > 0 && - extra.bottomRightStack[extra.bottomRightStack.length - 1].trailingComments && - extra.bottomRightStack[extra.bottomRightStack.length - 1].trailingComments[0].range[0] >= node.range[1]) { - trailingComments = extra.bottomRightStack[extra.bottomRightStack.length - 1].trailingComments; - delete extra.bottomRightStack[extra.bottomRightStack.length - 1].trailingComments; - } - } - - // Eating the stack. - while (extra.bottomRightStack.length > 0 && extra.bottomRightStack[extra.bottomRightStack.length - 1].range[0] >= node.range[0]) { - lastChild = extra.bottomRightStack.pop(); - } - - if (lastChild) { - if (lastChild.leadingComments && lastChild.leadingComments[lastChild.leadingComments.length - 1].range[1] <= node.range[0]) { - node.leadingComments = lastChild.leadingComments; - delete lastChild.leadingComments; - } - } else if (extra.leadingComments.length > 0 && extra.leadingComments[extra.leadingComments.length - 1].range[1] <= node.range[0]) { - node.leadingComments = extra.leadingComments; - extra.leadingComments = []; - } - - - if (trailingComments) { - node.trailingComments = trailingComments; - } - - extra.bottomRightStack.push(node); - }, - - markEnd: function (node, startToken) { - if (extra.range) { - node.range = [startToken.start, index]; - } - if (extra.loc) { - node.loc = new SourceLocation( - startToken.startLineNumber === undefined ? startToken.lineNumber : startToken.startLineNumber, - startToken.start - (startToken.startLineStart === undefined ? startToken.lineStart : startToken.startLineStart), - lineNumber, - index - lineStart - ); - this.postProcess(node); - } - - if (extra.attachComment) { - this.processComment(node); - } - return node; - }, - - postProcess: function (node) { - if (extra.source) { - node.loc.source = extra.source; - } - return node; - }, - - createArrayExpression: function (elements) { - return { - type: Syntax.ArrayExpression, - elements: elements - }; - }, - - createAssignmentExpression: function (operator, left, right) { - return { - type: Syntax.AssignmentExpression, - operator: operator, - left: left, - right: right - }; - }, - - createBinaryExpression: function (operator, left, right) { - var type = (operator === '||' || operator === '&&') ? Syntax.LogicalExpression : - Syntax.BinaryExpression; - return { - type: type, - operator: operator, - left: left, - right: right - }; - }, - - createBlockStatement: function (body) { - return { - type: Syntax.BlockStatement, - body: body - }; - }, - - createBreakStatement: function (label) { - return { - type: Syntax.BreakStatement, - label: label - }; - }, - - createCallExpression: function (callee, args) { - return { - type: Syntax.CallExpression, - callee: callee, - 'arguments': args - }; - }, - - createCatchClause: function (param, body) { - return { - type: Syntax.CatchClause, - param: param, - body: body - }; - }, - - createConditionalExpression: function (test, consequent, alternate) { - return { - type: Syntax.ConditionalExpression, - test: test, - consequent: consequent, - alternate: alternate - }; - }, - - createContinueStatement: function (label) { - return { - type: Syntax.ContinueStatement, - label: label - }; - }, - - createDebuggerStatement: function () { - return { - type: Syntax.DebuggerStatement - }; - }, - - createDoWhileStatement: function (body, test) { - return { - type: Syntax.DoWhileStatement, - body: body, - test: test - }; - }, - - createEmptyStatement: function () { - return { - type: Syntax.EmptyStatement - }; - }, - - createExpressionStatement: function (expression) { - return { - type: Syntax.ExpressionStatement, - expression: expression - }; - }, - - createForStatement: function (init, test, update, body) { - return { - type: Syntax.ForStatement, - init: init, - test: test, - update: update, - body: body - }; - }, - - createForInStatement: function (left, right, body) { - return { - type: Syntax.ForInStatement, - left: left, - right: right, - body: body, - each: false - }; - }, - - createFunctionDeclaration: function (id, params, defaults, body) { - return { - type: Syntax.FunctionDeclaration, - id: id, - params: params, - defaults: defaults, - body: body, - rest: null, - generator: false, - expression: false - }; - }, - - createFunctionExpression: function (id, params, defaults, body) { - return { - type: Syntax.FunctionExpression, - id: id, - params: params, - defaults: defaults, - body: body, - rest: null, - generator: false, - expression: false - }; - }, - - createIdentifier: function (name) { - return { - type: Syntax.Identifier, - name: name - }; - }, - - createIfStatement: function (test, consequent, alternate) { - return { - type: Syntax.IfStatement, - test: test, - consequent: consequent, - alternate: alternate - }; - }, - - createLabeledStatement: function (label, body) { - return { - type: Syntax.LabeledStatement, - label: label, - body: body - }; - }, - - createLiteral: function (token) { - return { - type: Syntax.Literal, - value: token.value, - raw: source.slice(token.start, token.end) - }; - }, - - createMemberExpression: function (accessor, object, property) { - return { - type: Syntax.MemberExpression, - computed: accessor === '[', - object: object, - property: property - }; - }, - - createNewExpression: function (callee, args) { - return { - type: Syntax.NewExpression, - callee: callee, - 'arguments': args - }; - }, - - createObjectExpression: function (properties) { - return { - type: Syntax.ObjectExpression, - properties: properties - }; - }, - - createPostfixExpression: function (operator, argument) { - return { - type: Syntax.UpdateExpression, - operator: operator, - argument: argument, - prefix: false - }; - }, - - createProgram: function (body) { - return { - type: Syntax.Program, - body: body - }; - }, - - createProperty: function (kind, key, value) { - return { - type: Syntax.Property, - key: key, - value: value, - kind: kind - }; - }, - - createReturnStatement: function (argument) { - return { - type: Syntax.ReturnStatement, - argument: argument - }; - }, - - createSequenceExpression: function (expressions) { - return { - type: Syntax.SequenceExpression, - expressions: expressions - }; - }, - - createSwitchCase: function (test, consequent) { - return { - type: Syntax.SwitchCase, - test: test, - consequent: consequent - }; - }, - - createSwitchStatement: function (discriminant, cases) { - return { - type: Syntax.SwitchStatement, - discriminant: discriminant, - cases: cases - }; - }, - - createThisExpression: function () { - return { - type: Syntax.ThisExpression - }; - }, - - createThrowStatement: function (argument) { - return { - type: Syntax.ThrowStatement, - argument: argument - }; - }, - - createTryStatement: function (block, guardedHandlers, handlers, finalizer) { - return { - type: Syntax.TryStatement, - block: block, - guardedHandlers: guardedHandlers, - handlers: handlers, - finalizer: finalizer - }; - }, - - createUnaryExpression: function (operator, argument) { - if (operator === '++' || operator === '--') { - return { - type: Syntax.UpdateExpression, - operator: operator, - argument: argument, - prefix: true - }; - } - return { - type: Syntax.UnaryExpression, - operator: operator, - argument: argument, - prefix: true - }; - }, - - createVariableDeclaration: function (declarations, kind) { - return { - type: Syntax.VariableDeclaration, - declarations: declarations, - kind: kind - }; - }, - - createVariableDeclarator: function (id, init) { - return { - type: Syntax.VariableDeclarator, - id: id, - init: init - }; - }, - - createWhileStatement: function (test, body) { - return { - type: Syntax.WhileStatement, - test: test, - body: body - }; - }, - - createWithStatement: function (object, body) { - return { - type: Syntax.WithStatement, - object: object, - body: body - }; + if (node.type === Syntax.Program) { + if (node.body.length > 0) { + return; + } } + + if (extra.trailingComments.length > 0) { + if (extra.trailingComments[0].range[0] >= node.range[1]) { + trailingComments = extra.trailingComments; + extra.trailingComments = []; + } else { + extra.trailingComments.length = 0; + } + } else { + if ( + extra.bottomRightStack.length > 0 && + extra.bottomRightStack[extra.bottomRightStack.length - 1] + .trailingComments && + extra.bottomRightStack[extra.bottomRightStack.length - 1] + .trailingComments[0].range[0] >= node.range[1] + ) { + trailingComments = + extra.bottomRightStack[extra.bottomRightStack.length - 1] + .trailingComments; + delete extra.bottomRightStack[extra.bottomRightStack.length - 1] + .trailingComments; + } + } + + // Eating the stack. + while ( + extra.bottomRightStack.length > 0 && + extra.bottomRightStack[extra.bottomRightStack.length - 1].range[0] >= + node.range[0] + ) { + lastChild = extra.bottomRightStack.pop(); + } + + if (lastChild) { + if ( + lastChild.leadingComments && + lastChild.leadingComments[lastChild.leadingComments.length - 1] + .range[1] <= node.range[0] + ) { + node.leadingComments = lastChild.leadingComments; + delete lastChild.leadingComments; + } + } else if ( + extra.leadingComments.length > 0 && + extra.leadingComments[extra.leadingComments.length - 1].range[1] <= + node.range[0] + ) { + node.leadingComments = extra.leadingComments; + extra.leadingComments = []; + } + + if (trailingComments) { + node.trailingComments = trailingComments; + } + + extra.bottomRightStack.push(node); + }, + + markEnd: function(node, startToken) { + if (extra.range) { + node.range = [startToken.start, index]; + } + if (extra.loc) { + node.loc = new SourceLocation( + startToken.startLineNumber === undefined + ? startToken.lineNumber + : startToken.startLineNumber, + startToken.start - + (startToken.startLineStart === undefined + ? startToken.lineStart + : startToken.startLineStart), + lineNumber, + index - lineStart + ); + this.postProcess(node); + } + + if (extra.attachComment) { + this.processComment(node); + } + return node; + }, + + postProcess: function(node) { + if (extra.source) { + node.loc.source = extra.source; + } + return node; + }, + + createArrayExpression: function(elements) { + return { + type: Syntax.ArrayExpression, + elements: elements, + }; + }, + + createAssignmentExpression: function(operator, left, right) { + return { + type: Syntax.AssignmentExpression, + operator: operator, + left: left, + right: right, + }; + }, + + createBinaryExpression: function(operator, left, right) { + var type = + operator === '||' || operator === '&&' + ? Syntax.LogicalExpression + : Syntax.BinaryExpression; + return { + type: type, + operator: operator, + left: left, + right: right, + }; + }, + + createBlockStatement: function(body) { + return { + type: Syntax.BlockStatement, + body: body, + }; + }, + + createBreakStatement: function(label) { + return { + type: Syntax.BreakStatement, + label: label, + }; + }, + + createCallExpression: function(callee, args) { + return { + type: Syntax.CallExpression, + callee: callee, + arguments: args, + }; + }, + + createCatchClause: function(param, body) { + return { + type: Syntax.CatchClause, + param: param, + body: body, + }; + }, + + createConditionalExpression: function(test, consequent, alternate) { + return { + type: Syntax.ConditionalExpression, + test: test, + consequent: consequent, + alternate: alternate, + }; + }, + + createContinueStatement: function(label) { + return { + type: Syntax.ContinueStatement, + label: label, + }; + }, + + createDebuggerStatement: function() { + return { + type: Syntax.DebuggerStatement, + }; + }, + + createDoWhileStatement: function(body, test) { + return { + type: Syntax.DoWhileStatement, + body: body, + test: test, + }; + }, + + createEmptyStatement: function() { + return { + type: Syntax.EmptyStatement, + }; + }, + + createExpressionStatement: function(expression) { + return { + type: Syntax.ExpressionStatement, + expression: expression, + }; + }, + + createForStatement: function(init, test, update, body) { + return { + type: Syntax.ForStatement, + init: init, + test: test, + update: update, + body: body, + }; + }, + + createForInStatement: function(left, right, body) { + return { + type: Syntax.ForInStatement, + left: left, + right: right, + body: body, + each: false, + }; + }, + + createFunctionDeclaration: function(id, params, defaults, body) { + return { + type: Syntax.FunctionDeclaration, + id: id, + params: params, + defaults: defaults, + body: body, + rest: null, + generator: false, + expression: false, + }; + }, + + createFunctionExpression: function(id, params, defaults, body) { + return { + type: Syntax.FunctionExpression, + id: id, + params: params, + defaults: defaults, + body: body, + rest: null, + generator: false, + expression: false, + }; + }, + + createIdentifier: function(name) { + return { + type: Syntax.Identifier, + name: name, + }; + }, + + createIfStatement: function(test, consequent, alternate) { + return { + type: Syntax.IfStatement, + test: test, + consequent: consequent, + alternate: alternate, + }; + }, + + createLabeledStatement: function(label, body) { + return { + type: Syntax.LabeledStatement, + label: label, + body: body, + }; + }, + + createLiteral: function(token) { + return { + type: Syntax.Literal, + value: token.value, + raw: source.slice(token.start, token.end), + }; + }, + + createMemberExpression: function(accessor, object, property) { + return { + type: Syntax.MemberExpression, + computed: accessor === '[', + object: object, + property: property, + }; + }, + + createNewExpression: function(callee, args) { + return { + type: Syntax.NewExpression, + callee: callee, + arguments: args, + }; + }, + + createObjectExpression: function(properties) { + return { + type: Syntax.ObjectExpression, + properties: properties, + }; + }, + + createPostfixExpression: function(operator, argument) { + return { + type: Syntax.UpdateExpression, + operator: operator, + argument: argument, + prefix: false, + }; + }, + + createProgram: function(body) { + return { + type: Syntax.Program, + body: body, + }; + }, + + createProperty: function(kind, key, value) { + return { + type: Syntax.Property, + key: key, + value: value, + kind: kind, + }; + }, + + createReturnStatement: function(argument) { + return { + type: Syntax.ReturnStatement, + argument: argument, + }; + }, + + createSequenceExpression: function(expressions) { + return { + type: Syntax.SequenceExpression, + expressions: expressions, + }; + }, + + createSwitchCase: function(test, consequent) { + return { + type: Syntax.SwitchCase, + test: test, + consequent: consequent, + }; + }, + + createSwitchStatement: function(discriminant, cases) { + return { + type: Syntax.SwitchStatement, + discriminant: discriminant, + cases: cases, + }; + }, + + createThisExpression: function() { + return { + type: Syntax.ThisExpression, + }; + }, + + createThrowStatement: function(argument) { + return { + type: Syntax.ThrowStatement, + argument: argument, + }; + }, + + createTryStatement: function( + block, + guardedHandlers, + handlers, + finalizer + ) { + return { + type: Syntax.TryStatement, + block: block, + guardedHandlers: guardedHandlers, + handlers: handlers, + finalizer: finalizer, + }; + }, + + createUnaryExpression: function(operator, argument) { + if (operator === '++' || operator === '--') { + return { + type: Syntax.UpdateExpression, + operator: operator, + argument: argument, + prefix: true, + }; + } + return { + type: Syntax.UnaryExpression, + operator: operator, + argument: argument, + prefix: true, + }; + }, + + createVariableDeclaration: function(declarations, kind) { + return { + type: Syntax.VariableDeclaration, + declarations: declarations, + kind: kind, + }; + }, + + createVariableDeclarator: function(id, init) { + return { + type: Syntax.VariableDeclarator, + id: id, + init: init, + }; + }, + + createWhileStatement: function(test, body) { + return { + type: Syntax.WhileStatement, + test: test, + body: body, + }; + }, + + createWithStatement: function(object, body) { + return { + type: Syntax.WithStatement, + object: object, + body: body, + }; + }, }; // Return true if there is a line terminator before the next token. function peekLineTerminator() { - var pos, line, start, found; + var pos, line, start, found; - pos = index; - line = lineNumber; - start = lineStart; - skipComment(); - found = lineNumber !== line; - index = pos; - lineNumber = line; - lineStart = start; + pos = index; + line = lineNumber; + start = lineStart; + skipComment(); + found = lineNumber !== line; + index = pos; + lineNumber = line; + lineStart = start; - return found; + return found; } // Throw an exception function throwError(token, messageFormat) { - var error, - args = Array.prototype.slice.call(arguments, 2), - msg = messageFormat.replace( - /%(\d)/g, - function (whole, index) { - assert(index < args.length, 'Message reference must be in range'); - return args[index]; - } - ); + var error, + args = Array.prototype.slice.call(arguments, 2), + msg = messageFormat.replace(/%(\d)/g, function(whole, index) { + assert(index < args.length, 'Message reference must be in range'); + return args[index]; + }); - if (typeof token.lineNumber === 'number') { - error = new Error('Line ' + token.lineNumber + ': ' + msg); - error.index = token.start; - error.lineNumber = token.lineNumber; - error.column = token.start - lineStart + 1; - } else { - error = new Error('Line ' + lineNumber + ': ' + msg); - error.index = index; - error.lineNumber = lineNumber; - error.column = index - lineStart + 1; - } + if (typeof token.lineNumber === 'number') { + error = new Error('Line ' + token.lineNumber + ': ' + msg); + error.index = token.start; + error.lineNumber = token.lineNumber; + error.column = token.start - lineStart + 1; + } else { + error = new Error('Line ' + lineNumber + ': ' + msg); + error.index = index; + error.lineNumber = lineNumber; + error.column = index - lineStart + 1; + } - error.description = msg; - throw error; + error.description = msg; + throw error; } function throwErrorTolerant() { - try { - throwError.apply(null, arguments); - } catch (e) { - if (extra.errors) { - extra.errors.push(e); - } else { - throw e; - } + try { + throwError.apply(null, arguments); + } catch (e) { + if (extra.errors) { + extra.errors.push(e); + } else { + throw e; } + } } - // Throw an exception because of the token. function throwUnexpected(token) { - if (token.type === Token.EOF) { - throwError(token, Messages.UnexpectedEOS); - } + if (token.type === Token.EOF) { + throwError(token, Messages.UnexpectedEOS); + } - if (token.type === Token.NumericLiteral) { - throwError(token, Messages.UnexpectedNumber); - } + if (token.type === Token.NumericLiteral) { + throwError(token, Messages.UnexpectedNumber); + } - if (token.type === Token.StringLiteral) { - throwError(token, Messages.UnexpectedString); - } + if (token.type === Token.StringLiteral) { + throwError(token, Messages.UnexpectedString); + } - if (token.type === Token.Identifier) { - throwError(token, Messages.UnexpectedIdentifier); - } + if (token.type === Token.Identifier) { + throwError(token, Messages.UnexpectedIdentifier); + } - if (token.type === Token.Keyword) { - if (isFutureReservedWord(token.value)) { - throwError(token, Messages.UnexpectedReserved); - } else if (strict && isStrictModeReservedWord(token.value)) { - throwErrorTolerant(token, Messages.StrictReservedWord); - return; - } - throwError(token, Messages.UnexpectedToken, token.value); + if (token.type === Token.Keyword) { + if (isFutureReservedWord(token.value)) { + throwError(token, Messages.UnexpectedReserved); + } else if (strict && isStrictModeReservedWord(token.value)) { + throwErrorTolerant(token, Messages.StrictReservedWord); + return; } - - // BooleanLiteral, NullLiteral, or Punctuator. throwError(token, Messages.UnexpectedToken, token.value); + } + + // BooleanLiteral, NullLiteral, or Punctuator. + throwError(token, Messages.UnexpectedToken, token.value); } // Expect the next token to match the specified punctuator. // If not, an exception will be thrown. function expect(value) { - var token = lex(); - if (token.type !== Token.Punctuator || token.value !== value) { - throwUnexpected(token); - } + var token = lex(); + if (token.type !== Token.Punctuator || token.value !== value) { + throwUnexpected(token); + } } // Expect the next token to match the specified keyword. // If not, an exception will be thrown. function expectKeyword(keyword) { - var token = lex(); - if (token.type !== Token.Keyword || token.value !== keyword) { - throwUnexpected(token); - } + var token = lex(); + if (token.type !== Token.Keyword || token.value !== keyword) { + throwUnexpected(token); + } } // Return true if the next token matches the specified punctuator. function match(value) { - return lookahead.type === Token.Punctuator && lookahead.value === value; + return lookahead.type === Token.Punctuator && lookahead.value === value; } // Return true if the next token matches the specified keyword function matchKeyword(keyword) { - return lookahead.type === Token.Keyword && lookahead.value === keyword; + return lookahead.type === Token.Keyword && lookahead.value === keyword; } // Return true if the next token is an assignment operator function matchAssign() { - var op; + var op; - if (lookahead.type !== Token.Punctuator) { - return false; - } - op = lookahead.value; - return op === '=' || - op === '*=' || - op === '/=' || - op === '%=' || - op === '+=' || - op === '-=' || - op === '<<=' || - op === '>>=' || - op === '>>>=' || - op === '&=' || - op === '^=' || - op === '|='; + if (lookahead.type !== Token.Punctuator) { + return false; + } + op = lookahead.value; + return ( + op === '=' || + op === '*=' || + op === '/=' || + op === '%=' || + op === '+=' || + op === '-=' || + op === '<<=' || + op === '>>=' || + op === '>>>=' || + op === '&=' || + op === '^=' || + op === '|=' + ); } function consumeSemicolon() { - var line; + var line; - // Catch the very common case first: immediately a semicolon (U+003B). - if (source.charCodeAt(index) === 0x3B || match(';')) { - lex(); - return; - } + // Catch the very common case first: immediately a semicolon (U+003B). + if (source.charCodeAt(index) === 0x3b || match(';')) { + lex(); + return; + } - line = lineNumber; - skipComment(); - if (lineNumber !== line) { - return; - } + line = lineNumber; + skipComment(); + if (lineNumber !== line) { + return; + } - if (lookahead.type !== Token.EOF && !match('}')) { - throwUnexpected(lookahead); - } + if (lookahead.type !== Token.EOF && !match('}')) { + throwUnexpected(lookahead); + } } // Return true if provided expression is LeftHandSideExpression function isLeftHandSide(expr) { - return expr.type === Syntax.Identifier || expr.type === Syntax.MemberExpression; + return ( + expr.type === Syntax.Identifier || expr.type === Syntax.MemberExpression + ); } // 11.1.4 Array Initialiser function parseArrayInitialiser() { - var elements = [], startToken; + var elements = [], + startToken; - startToken = lookahead; - expect('['); + startToken = lookahead; + expect('['); - while (!match(']')) { - if (match(',')) { - lex(); - elements.push(null); - } else { - elements.push(parseAssignmentExpression()); + while (!match(']')) { + if (match(',')) { + lex(); + elements.push(null); + } else { + elements.push(parseAssignmentExpression()); - if (!match(']')) { - expect(','); - } - } + if (!match(']')) { + expect(','); + } } + } - lex(); + lex(); - return delegate.markEnd(delegate.createArrayExpression(elements), startToken); + return delegate.markEnd( + delegate.createArrayExpression(elements), + startToken + ); } // 11.1.5 Object Initialiser function parsePropertyFunction(param, first) { - var previousStrict, body, startToken; + var previousStrict, body, startToken; - previousStrict = strict; - startToken = lookahead; - body = parseFunctionSourceElements(); - if (first && strict && isRestrictedWord(param[0].name)) { - throwErrorTolerant(first, Messages.StrictParamName); - } - strict = previousStrict; - return delegate.markEnd(delegate.createFunctionExpression(null, param, [], body), startToken); + previousStrict = strict; + startToken = lookahead; + body = parseFunctionSourceElements(); + if (first && strict && isRestrictedWord(param[0].name)) { + throwErrorTolerant(first, Messages.StrictParamName); + } + strict = previousStrict; + return delegate.markEnd( + delegate.createFunctionExpression(null, param, [], body), + startToken + ); } function parseObjectPropertyKey() { - var token, startToken; + var token, startToken; - startToken = lookahead; - token = lex(); + startToken = lookahead; + token = lex(); - // Note: This function is called only from parseObjectProperty(), where - // EOF and Punctuator tokens are already filtered out. + // Note: This function is called only from parseObjectProperty(), where + // EOF and Punctuator tokens are already filtered out. - if (token.type === Token.StringLiteral || token.type === Token.NumericLiteral) { - if (strict && token.octal) { - throwErrorTolerant(token, Messages.StrictOctalLiteral); - } - return delegate.markEnd(delegate.createLiteral(token), startToken); + if ( + token.type === Token.StringLiteral || + token.type === Token.NumericLiteral + ) { + if (strict && token.octal) { + throwErrorTolerant(token, Messages.StrictOctalLiteral); } + return delegate.markEnd(delegate.createLiteral(token), startToken); + } - return delegate.markEnd(delegate.createIdentifier(token.value), startToken); + return delegate.markEnd( + delegate.createIdentifier(token.value), + startToken + ); } function parseObjectProperty() { - var token, key, id, value, param, startToken; + var token, key, id, value, param, startToken; - token = lookahead; - startToken = lookahead; + token = lookahead; + startToken = lookahead; - if (token.type === Token.Identifier) { + if (token.type === Token.Identifier) { + id = parseObjectPropertyKey(); - id = parseObjectPropertyKey(); + // Property Assignment: Getter and Setter. - // Property Assignment: Getter and Setter. - - if (token.value === 'get' && !match(':')) { - key = parseObjectPropertyKey(); - expect('('); - expect(')'); - value = parsePropertyFunction([]); - return delegate.markEnd(delegate.createProperty('get', key, value), startToken); - } - if (token.value === 'set' && !match(':')) { - key = parseObjectPropertyKey(); - expect('('); - token = lookahead; - if (token.type !== Token.Identifier) { - expect(')'); - throwErrorTolerant(token, Messages.UnexpectedToken, token.value); - value = parsePropertyFunction([]); - } else { - param = [ parseVariableIdentifier() ]; - expect(')'); - value = parsePropertyFunction(param, token); - } - return delegate.markEnd(delegate.createProperty('set', key, value), startToken); - } - expect(':'); - value = parseAssignmentExpression(); - return delegate.markEnd(delegate.createProperty('init', id, value), startToken); + if (token.value === 'get' && !match(':')) { + key = parseObjectPropertyKey(); + expect('('); + expect(')'); + value = parsePropertyFunction([]); + return delegate.markEnd( + delegate.createProperty('get', key, value), + startToken + ); } - if (token.type === Token.EOF || token.type === Token.Punctuator) { - throwUnexpected(token); - } else { - key = parseObjectPropertyKey(); - expect(':'); - value = parseAssignmentExpression(); - return delegate.markEnd(delegate.createProperty('init', key, value), startToken); + if (token.value === 'set' && !match(':')) { + key = parseObjectPropertyKey(); + expect('('); + token = lookahead; + if (token.type !== Token.Identifier) { + expect(')'); + throwErrorTolerant(token, Messages.UnexpectedToken, token.value); + value = parsePropertyFunction([]); + } else { + param = [parseVariableIdentifier()]; + expect(')'); + value = parsePropertyFunction(param, token); + } + return delegate.markEnd( + delegate.createProperty('set', key, value), + startToken + ); } + expect(':'); + value = parseAssignmentExpression(); + return delegate.markEnd( + delegate.createProperty('init', id, value), + startToken + ); + } + if (token.type === Token.EOF || token.type === Token.Punctuator) { + throwUnexpected(token); + } else { + key = parseObjectPropertyKey(); + expect(':'); + value = parseAssignmentExpression(); + return delegate.markEnd( + delegate.createProperty('init', key, value), + startToken + ); + } } function parseObjectInitialiser() { - var properties = [], property, name, key, kind, map = {}, toString = String, startToken; + var properties = [], + property, + name, + key, + kind, + map = {}, + toString = String, + startToken; - startToken = lookahead; + startToken = lookahead; - expect('{'); + expect('{'); - while (!match('}')) { - property = parseObjectProperty(); + while (!match('}')) { + property = parseObjectProperty(); - if (property.key.type === Syntax.Identifier) { - name = property.key.name; - } else { - name = toString(property.key.value); + if (property.key.type === Syntax.Identifier) { + name = property.key.name; + } else { + name = toString(property.key.value); + } + kind = + property.kind === 'init' + ? PropertyKind.Data + : property.kind === 'get' + ? PropertyKind.Get + : PropertyKind.Set; + + key = '$' + name; + if (Object.prototype.hasOwnProperty.call(map, key)) { + if (map[key] === PropertyKind.Data) { + if (strict && kind === PropertyKind.Data) { + throwErrorTolerant({}, Messages.StrictDuplicateProperty); + } else if (kind !== PropertyKind.Data) { + throwErrorTolerant({}, Messages.AccessorDataProperty); } - kind = (property.kind === 'init') ? PropertyKind.Data : (property.kind === 'get') ? PropertyKind.Get : PropertyKind.Set; - - key = '$' + name; - if (Object.prototype.hasOwnProperty.call(map, key)) { - if (map[key] === PropertyKind.Data) { - if (strict && kind === PropertyKind.Data) { - throwErrorTolerant({}, Messages.StrictDuplicateProperty); - } else if (kind !== PropertyKind.Data) { - throwErrorTolerant({}, Messages.AccessorDataProperty); - } - } else { - if (kind === PropertyKind.Data) { - throwErrorTolerant({}, Messages.AccessorDataProperty); - } else if (map[key] & kind) { - throwErrorTolerant({}, Messages.AccessorGetSet); - } - } - map[key] |= kind; - } else { - map[key] = kind; - } - - properties.push(property); - - if (!match('}')) { - expect(','); + } else { + if (kind === PropertyKind.Data) { + throwErrorTolerant({}, Messages.AccessorDataProperty); + } else if (map[key] & kind) { + throwErrorTolerant({}, Messages.AccessorGetSet); } + } + map[key] |= kind; + } else { + map[key] = kind; } - expect('}'); + properties.push(property); - return delegate.markEnd(delegate.createObjectExpression(properties), startToken); + if (!match('}')) { + expect(','); + } + } + + expect('}'); + + return delegate.markEnd( + delegate.createObjectExpression(properties), + startToken + ); } // 11.1.6 The Grouping Operator function parseGroupExpression() { - var expr; + var expr; - expect('('); + expect('('); - expr = parseExpression(); + expr = parseExpression(); - expect(')'); + expect(')'); - return expr; + return expr; } - // 11.1 Primary Expressions function parsePrimaryExpression() { - var type, token, expr, startToken; + var type, token, expr, startToken; - if (match('(')) { - return parseGroupExpression(); + if (match('(')) { + return parseGroupExpression(); + } + + if (match('[')) { + return parseArrayInitialiser(); + } + + if (match('{')) { + return parseObjectInitialiser(); + } + + type = lookahead.type; + startToken = lookahead; + + if (type === Token.Identifier) { + expr = delegate.createIdentifier(lex().value); + } else if ( + type === Token.StringLiteral || + type === Token.NumericLiteral + ) { + if (strict && lookahead.octal) { + throwErrorTolerant(lookahead, Messages.StrictOctalLiteral); } - - if (match('[')) { - return parseArrayInitialiser(); + expr = delegate.createLiteral(lex()); + } else if (type === Token.Keyword) { + if (matchKeyword('function')) { + return parseFunctionExpression(); } - - if (match('{')) { - return parseObjectInitialiser(); - } - - type = lookahead.type; - startToken = lookahead; - - if (type === Token.Identifier) { - expr = delegate.createIdentifier(lex().value); - } else if (type === Token.StringLiteral || type === Token.NumericLiteral) { - if (strict && lookahead.octal) { - throwErrorTolerant(lookahead, Messages.StrictOctalLiteral); - } - expr = delegate.createLiteral(lex()); - } else if (type === Token.Keyword) { - if (matchKeyword('function')) { - return parseFunctionExpression(); - } - if (matchKeyword('this')) { - lex(); - expr = delegate.createThisExpression(); - } else { - throwUnexpected(lex()); - } - } else if (type === Token.BooleanLiteral) { - token = lex(); - token.value = (token.value === 'true'); - expr = delegate.createLiteral(token); - } else if (type === Token.NullLiteral) { - token = lex(); - token.value = null; - expr = delegate.createLiteral(token); - } else if (match('/') || match('/=')) { - if (typeof extra.tokens !== 'undefined') { - expr = delegate.createLiteral(collectRegex()); - } else { - expr = delegate.createLiteral(scanRegExp()); - } - peek(); + if (matchKeyword('this')) { + lex(); + expr = delegate.createThisExpression(); } else { - throwUnexpected(lex()); + throwUnexpected(lex()); } + } else if (type === Token.BooleanLiteral) { + token = lex(); + token.value = token.value === 'true'; + expr = delegate.createLiteral(token); + } else if (type === Token.NullLiteral) { + token = lex(); + token.value = null; + expr = delegate.createLiteral(token); + } else if (match('/') || match('/=')) { + if (typeof extra.tokens !== 'undefined') { + expr = delegate.createLiteral(collectRegex()); + } else { + expr = delegate.createLiteral(scanRegExp()); + } + peek(); + } else { + throwUnexpected(lex()); + } - return delegate.markEnd(expr, startToken); + return delegate.markEnd(expr, startToken); } // 11.2 Left-Hand-Side Expressions function parseArguments() { - var args = []; + var args = []; - expect('('); + expect('('); - if (!match(')')) { - while (index < length) { - args.push(parseAssignmentExpression()); - if (match(')')) { - break; - } - expect(','); - } + if (!match(')')) { + while (index < length) { + args.push(parseAssignmentExpression()); + if (match(')')) { + break; + } + expect(','); } + } - expect(')'); + expect(')'); - return args; + return args; } function parseNonComputedProperty() { - var token, startToken; + var token, startToken; - startToken = lookahead; - token = lex(); + startToken = lookahead; + token = lex(); - if (!isIdentifierName(token)) { - throwUnexpected(token); - } + if (!isIdentifierName(token)) { + throwUnexpected(token); + } - return delegate.markEnd(delegate.createIdentifier(token.value), startToken); + return delegate.markEnd( + delegate.createIdentifier(token.value), + startToken + ); } function parseNonComputedMember() { - expect('.'); + expect('.'); - return parseNonComputedProperty(); + return parseNonComputedProperty(); } function parseComputedMember() { - var expr; + var expr; - expect('['); + expect('['); - expr = parseExpression(); + expr = parseExpression(); - expect(']'); + expect(']'); - return expr; + return expr; } function parseNewExpression() { - var callee, args, startToken; + var callee, args, startToken; - startToken = lookahead; - expectKeyword('new'); - callee = parseLeftHandSideExpression(); - args = match('(') ? parseArguments() : []; + startToken = lookahead; + expectKeyword('new'); + callee = parseLeftHandSideExpression(); + args = match('(') ? parseArguments() : []; - return delegate.markEnd(delegate.createNewExpression(callee, args), startToken); + return delegate.markEnd( + delegate.createNewExpression(callee, args), + startToken + ); } function parseLeftHandSideExpressionAllowCall() { - var previousAllowIn, expr, args, property, startToken; + var previousAllowIn, expr, args, property, startToken; - startToken = lookahead; + startToken = lookahead; - previousAllowIn = state.allowIn; - state.allowIn = true; - expr = matchKeyword('new') ? parseNewExpression() : parsePrimaryExpression(); - state.allowIn = previousAllowIn; + previousAllowIn = state.allowIn; + state.allowIn = true; + expr = matchKeyword('new') + ? parseNewExpression() + : parsePrimaryExpression(); + state.allowIn = previousAllowIn; - for (;;) { - if (match('.')) { - property = parseNonComputedMember(); - expr = delegate.createMemberExpression('.', expr, property); - } else if (match('(')) { - args = parseArguments(); - expr = delegate.createCallExpression(expr, args); - } else if (match('[')) { - property = parseComputedMember(); - expr = delegate.createMemberExpression('[', expr, property); - } else { - break; - } - delegate.markEnd(expr, startToken); + for (;;) { + if (match('.')) { + property = parseNonComputedMember(); + expr = delegate.createMemberExpression('.', expr, property); + } else if (match('(')) { + args = parseArguments(); + expr = delegate.createCallExpression(expr, args); + } else if (match('[')) { + property = parseComputedMember(); + expr = delegate.createMemberExpression('[', expr, property); + } else { + break; } + delegate.markEnd(expr, startToken); + } - return expr; + return expr; } function parseLeftHandSideExpression() { - var previousAllowIn, expr, property, startToken; + var previousAllowIn, expr, property, startToken; - startToken = lookahead; + startToken = lookahead; - previousAllowIn = state.allowIn; - expr = matchKeyword('new') ? parseNewExpression() : parsePrimaryExpression(); - state.allowIn = previousAllowIn; + previousAllowIn = state.allowIn; + expr = matchKeyword('new') + ? parseNewExpression() + : parsePrimaryExpression(); + state.allowIn = previousAllowIn; - while (match('.') || match('[')) { - if (match('[')) { - property = parseComputedMember(); - expr = delegate.createMemberExpression('[', expr, property); - } else { - property = parseNonComputedMember(); - expr = delegate.createMemberExpression('.', expr, property); - } - delegate.markEnd(expr, startToken); + while (match('.') || match('[')) { + if (match('[')) { + property = parseComputedMember(); + expr = delegate.createMemberExpression('[', expr, property); + } else { + property = parseNonComputedMember(); + expr = delegate.createMemberExpression('.', expr, property); } + delegate.markEnd(expr, startToken); + } - return expr; + return expr; } // 11.3 Postfix Expressions function parsePostfixExpression() { - var expr, token, startToken = lookahead; + var expr, + token, + startToken = lookahead; - expr = parseLeftHandSideExpressionAllowCall(); + expr = parseLeftHandSideExpressionAllowCall(); - if (lookahead.type === Token.Punctuator) { - if ((match('++') || match('--')) && !peekLineTerminator()) { - // 11.3.1, 11.3.2 - if (strict && expr.type === Syntax.Identifier && isRestrictedWord(expr.name)) { - throwErrorTolerant({}, Messages.StrictLHSPostfix); - } + if (lookahead.type === Token.Punctuator) { + if ((match('++') || match('--')) && !peekLineTerminator()) { + // 11.3.1, 11.3.2 + if ( + strict && + expr.type === Syntax.Identifier && + isRestrictedWord(expr.name) + ) { + throwErrorTolerant({}, Messages.StrictLHSPostfix); + } - if (!isLeftHandSide(expr)) { - throwErrorTolerant({}, Messages.InvalidLHSInAssignment); - } + if (!isLeftHandSide(expr)) { + throwErrorTolerant({}, Messages.InvalidLHSInAssignment); + } - token = lex(); - expr = delegate.markEnd(delegate.createPostfixExpression(token.value, expr), startToken); - } + token = lex(); + expr = delegate.markEnd( + delegate.createPostfixExpression(token.value, expr), + startToken + ); } + } - return expr; + return expr; } // 11.4 Unary Operators function parseUnaryExpression() { - var token, expr, startToken; + var token, expr, startToken; - if (lookahead.type !== Token.Punctuator && lookahead.type !== Token.Keyword) { - expr = parsePostfixExpression(); - } else if (match('++') || match('--')) { - startToken = lookahead; - token = lex(); - expr = parseUnaryExpression(); - // 11.4.4, 11.4.5 - if (strict && expr.type === Syntax.Identifier && isRestrictedWord(expr.name)) { - throwErrorTolerant({}, Messages.StrictLHSPrefix); - } - - if (!isLeftHandSide(expr)) { - throwErrorTolerant({}, Messages.InvalidLHSInAssignment); - } - - expr = delegate.createUnaryExpression(token.value, expr); - expr = delegate.markEnd(expr, startToken); - } else if (match('+') || match('-') || match('~') || match('!')) { - startToken = lookahead; - token = lex(); - expr = parseUnaryExpression(); - expr = delegate.createUnaryExpression(token.value, expr); - expr = delegate.markEnd(expr, startToken); - } else if (matchKeyword('delete') || matchKeyword('void') || matchKeyword('typeof')) { - startToken = lookahead; - token = lex(); - expr = parseUnaryExpression(); - expr = delegate.createUnaryExpression(token.value, expr); - expr = delegate.markEnd(expr, startToken); - if (strict && expr.operator === 'delete' && expr.argument.type === Syntax.Identifier) { - throwErrorTolerant({}, Messages.StrictDelete); - } - } else { - expr = parsePostfixExpression(); + if ( + lookahead.type !== Token.Punctuator && + lookahead.type !== Token.Keyword + ) { + expr = parsePostfixExpression(); + } else if (match('++') || match('--')) { + startToken = lookahead; + token = lex(); + expr = parseUnaryExpression(); + // 11.4.4, 11.4.5 + if ( + strict && + expr.type === Syntax.Identifier && + isRestrictedWord(expr.name) + ) { + throwErrorTolerant({}, Messages.StrictLHSPrefix); } - return expr; + if (!isLeftHandSide(expr)) { + throwErrorTolerant({}, Messages.InvalidLHSInAssignment); + } + + expr = delegate.createUnaryExpression(token.value, expr); + expr = delegate.markEnd(expr, startToken); + } else if (match('+') || match('-') || match('~') || match('!')) { + startToken = lookahead; + token = lex(); + expr = parseUnaryExpression(); + expr = delegate.createUnaryExpression(token.value, expr); + expr = delegate.markEnd(expr, startToken); + } else if ( + matchKeyword('delete') || + matchKeyword('void') || + matchKeyword('typeof') + ) { + startToken = lookahead; + token = lex(); + expr = parseUnaryExpression(); + expr = delegate.createUnaryExpression(token.value, expr); + expr = delegate.markEnd(expr, startToken); + if ( + strict && + expr.operator === 'delete' && + expr.argument.type === Syntax.Identifier + ) { + throwErrorTolerant({}, Messages.StrictDelete); + } + } else { + expr = parsePostfixExpression(); + } + + return expr; } function binaryPrecedence(token, allowIn) { - var prec = 0; + var prec = 0; - if (token.type !== Token.Punctuator && token.type !== Token.Keyword) { - return 0; - } + if (token.type !== Token.Punctuator && token.type !== Token.Keyword) { + return 0; + } - switch (token.value) { + switch (token.value) { case '||': - prec = 1; - break; + prec = 1; + break; case '&&': - prec = 2; - break; + prec = 2; + break; case '|': - prec = 3; - break; + prec = 3; + break; case '^': - prec = 4; - break; + prec = 4; + break; case '&': - prec = 5; - break; + prec = 5; + break; case '==': case '!=': case '===': case '!==': - prec = 6; - break; + prec = 6; + break; case '<': case '>': case '<=': case '>=': case 'instanceof': - prec = 7; - break; + prec = 7; + break; case 'in': - prec = allowIn ? 7 : 0; - break; + prec = allowIn ? 7 : 0; + break; case '<<': case '>>': case '>>>': - prec = 8; - break; + prec = 8; + break; case '+': case '-': - prec = 9; - break; + prec = 9; + break; case '*': case '/': case '%': - prec = 11; - break; + prec = 11; + break; default: - break; - } + break; + } - return prec; + return prec; } // 11.5 Multiplicative Operators @@ -2470,234 +2730,256 @@ parseStatement: true, parseSourceElement: true */ // 11.11 Binary Logical Operators function parseBinaryExpression() { - var marker, markers, expr, token, prec, stack, right, operator, left, i; + var marker, markers, expr, token, prec, stack, right, operator, left, i; - marker = lookahead; - left = parseUnaryExpression(); + marker = lookahead; + left = parseUnaryExpression(); - token = lookahead; - prec = binaryPrecedence(token, state.allowIn); - if (prec === 0) { - return left; + token = lookahead; + prec = binaryPrecedence(token, state.allowIn); + if (prec === 0) { + return left; + } + token.prec = prec; + lex(); + + markers = [marker, lookahead]; + right = parseUnaryExpression(); + + stack = [left, token, right]; + + while ((prec = binaryPrecedence(lookahead, state.allowIn)) > 0) { + // Reduce: make a binary expression from the three topmost entries. + while (stack.length > 2 && prec <= stack[stack.length - 2].prec) { + right = stack.pop(); + operator = stack.pop().value; + left = stack.pop(); + expr = delegate.createBinaryExpression(operator, left, right); + markers.pop(); + marker = markers[markers.length - 1]; + delegate.markEnd(expr, marker); + stack.push(expr); } + + // Shift. + token = lex(); token.prec = prec; - lex(); + stack.push(token); + markers.push(lookahead); + expr = parseUnaryExpression(); + stack.push(expr); + } - markers = [marker, lookahead]; - right = parseUnaryExpression(); + // Final reduce to clean-up the stack. + i = stack.length - 1; + expr = stack[i]; + markers.pop(); + while (i > 1) { + expr = delegate.createBinaryExpression( + stack[i - 1].value, + stack[i - 2], + expr + ); + i -= 2; + marker = markers.pop(); + delegate.markEnd(expr, marker); + } - stack = [left, token, right]; - - while ((prec = binaryPrecedence(lookahead, state.allowIn)) > 0) { - - // Reduce: make a binary expression from the three topmost entries. - while ((stack.length > 2) && (prec <= stack[stack.length - 2].prec)) { - right = stack.pop(); - operator = stack.pop().value; - left = stack.pop(); - expr = delegate.createBinaryExpression(operator, left, right); - markers.pop(); - marker = markers[markers.length - 1]; - delegate.markEnd(expr, marker); - stack.push(expr); - } - - // Shift. - token = lex(); - token.prec = prec; - stack.push(token); - markers.push(lookahead); - expr = parseUnaryExpression(); - stack.push(expr); - } - - // Final reduce to clean-up the stack. - i = stack.length - 1; - expr = stack[i]; - markers.pop(); - while (i > 1) { - expr = delegate.createBinaryExpression(stack[i - 1].value, stack[i - 2], expr); - i -= 2; - marker = markers.pop(); - delegate.markEnd(expr, marker); - } - - return expr; + return expr; } - // 11.12 Conditional Operator function parseConditionalExpression() { - var expr, previousAllowIn, consequent, alternate, startToken; + var expr, previousAllowIn, consequent, alternate, startToken; - startToken = lookahead; + startToken = lookahead; - expr = parseBinaryExpression(); + expr = parseBinaryExpression(); - if (match('?')) { - lex(); - previousAllowIn = state.allowIn; - state.allowIn = true; - consequent = parseAssignmentExpression(); - state.allowIn = previousAllowIn; - expect(':'); - alternate = parseAssignmentExpression(); + if (match('?')) { + lex(); + previousAllowIn = state.allowIn; + state.allowIn = true; + consequent = parseAssignmentExpression(); + state.allowIn = previousAllowIn; + expect(':'); + alternate = parseAssignmentExpression(); - expr = delegate.createConditionalExpression(expr, consequent, alternate); - delegate.markEnd(expr, startToken); - } + expr = delegate.createConditionalExpression( + expr, + consequent, + alternate + ); + delegate.markEnd(expr, startToken); + } - return expr; + return expr; } // 11.13 Assignment Operators function parseAssignmentExpression() { - var token, left, right, node, startToken; + var token, left, right, node, startToken; - token = lookahead; - startToken = lookahead; + token = lookahead; + startToken = lookahead; - node = left = parseConditionalExpression(); + node = left = parseConditionalExpression(); - if (matchAssign()) { - // LeftHandSideExpression - if (!isLeftHandSide(left)) { - throwErrorTolerant({}, Messages.InvalidLHSInAssignment); - } - - // 11.13.1 - if (strict && left.type === Syntax.Identifier && isRestrictedWord(left.name)) { - throwErrorTolerant(token, Messages.StrictLHSAssignment); - } - - token = lex(); - right = parseAssignmentExpression(); - node = delegate.markEnd(delegate.createAssignmentExpression(token.value, left, right), startToken); + if (matchAssign()) { + // LeftHandSideExpression + if (!isLeftHandSide(left)) { + throwErrorTolerant({}, Messages.InvalidLHSInAssignment); } - return node; + // 11.13.1 + if ( + strict && + left.type === Syntax.Identifier && + isRestrictedWord(left.name) + ) { + throwErrorTolerant(token, Messages.StrictLHSAssignment); + } + + token = lex(); + right = parseAssignmentExpression(); + node = delegate.markEnd( + delegate.createAssignmentExpression(token.value, left, right), + startToken + ); + } + + return node; } // 11.14 Comma Operator function parseExpression() { - var expr, startToken = lookahead; + var expr, + startToken = lookahead; - expr = parseAssignmentExpression(); + expr = parseAssignmentExpression(); - if (match(',')) { - expr = delegate.createSequenceExpression([ expr ]); + if (match(',')) { + expr = delegate.createSequenceExpression([expr]); - while (index < length) { - if (!match(',')) { - break; - } - lex(); - expr.expressions.push(parseAssignmentExpression()); - } - - delegate.markEnd(expr, startToken); + while (index < length) { + if (!match(',')) { + break; + } + lex(); + expr.expressions.push(parseAssignmentExpression()); } - return expr; + delegate.markEnd(expr, startToken); + } + + return expr; } // 12.1 Block function parseStatementList() { - var list = [], - statement; + var list = [], + statement; - while (index < length) { - if (match('}')) { - break; - } - statement = parseSourceElement(); - if (typeof statement === 'undefined') { - break; - } - list.push(statement); + while (index < length) { + if (match('}')) { + break; } + statement = parseSourceElement(); + if (typeof statement === 'undefined') { + break; + } + list.push(statement); + } - return list; + return list; } function parseBlock() { - var block, startToken; + var block, startToken; - startToken = lookahead; - expect('{'); + startToken = lookahead; + expect('{'); - block = parseStatementList(); + block = parseStatementList(); - expect('}'); + expect('}'); - return delegate.markEnd(delegate.createBlockStatement(block), startToken); + return delegate.markEnd(delegate.createBlockStatement(block), startToken); } // 12.2 Variable Statement function parseVariableIdentifier() { - var token, startToken; + var token, startToken; - startToken = lookahead; - token = lex(); + startToken = lookahead; + token = lex(); - if (token.type !== Token.Identifier) { - throwUnexpected(token); - } + if (token.type !== Token.Identifier) { + throwUnexpected(token); + } - return delegate.markEnd(delegate.createIdentifier(token.value), startToken); + return delegate.markEnd( + delegate.createIdentifier(token.value), + startToken + ); } function parseVariableDeclaration(kind) { - var init = null, id, startToken; + var init = null, + id, + startToken; - startToken = lookahead; - id = parseVariableIdentifier(); + startToken = lookahead; + id = parseVariableIdentifier(); - // 12.2.1 - if (strict && isRestrictedWord(id.name)) { - throwErrorTolerant({}, Messages.StrictVarName); - } + // 12.2.1 + if (strict && isRestrictedWord(id.name)) { + throwErrorTolerant({}, Messages.StrictVarName); + } - if (kind === 'const') { - expect('='); - init = parseAssignmentExpression(); - } else if (match('=')) { - lex(); - init = parseAssignmentExpression(); - } + if (kind === 'const') { + expect('='); + init = parseAssignmentExpression(); + } else if (match('=')) { + lex(); + init = parseAssignmentExpression(); + } - return delegate.markEnd(delegate.createVariableDeclarator(id, init), startToken); + return delegate.markEnd( + delegate.createVariableDeclarator(id, init), + startToken + ); } function parseVariableDeclarationList(kind) { - var list = []; + var list = []; - do { - list.push(parseVariableDeclaration(kind)); - if (!match(',')) { - break; - } - lex(); - } while (index < length); + do { + list.push(parseVariableDeclaration(kind)); + if (!match(',')) { + break; + } + lex(); + } while (index < length); - return list; + return list; } function parseVariableStatement() { - var declarations; + var declarations; - expectKeyword('var'); + expectKeyword('var'); - declarations = parseVariableDeclarationList(); + declarations = parseVariableDeclarationList(); - consumeSemicolon(); + consumeSemicolon(); - return delegate.createVariableDeclaration(declarations, 'var'); + return delegate.createVariableDeclaration(declarations, 'var'); } // kind may be `const` or `let` @@ -2705,1025 +2987,1100 @@ parseStatement: true, parseSourceElement: true */ // see http://wiki.ecmascript.org/doku.php?id=harmony:const // and http://wiki.ecmascript.org/doku.php?id=harmony:let function parseConstLetDeclaration(kind) { - var declarations, startToken; + var declarations, startToken; - startToken = lookahead; + startToken = lookahead; - expectKeyword(kind); + expectKeyword(kind); - declarations = parseVariableDeclarationList(kind); + declarations = parseVariableDeclarationList(kind); - consumeSemicolon(); + consumeSemicolon(); - return delegate.markEnd(delegate.createVariableDeclaration(declarations, kind), startToken); + return delegate.markEnd( + delegate.createVariableDeclaration(declarations, kind), + startToken + ); } // 12.3 Empty Statement function parseEmptyStatement() { - expect(';'); - return delegate.createEmptyStatement(); + expect(';'); + return delegate.createEmptyStatement(); } // 12.4 Expression Statement function parseExpressionStatement() { - var expr = parseExpression(); - consumeSemicolon(); - return delegate.createExpressionStatement(expr); + var expr = parseExpression(); + consumeSemicolon(); + return delegate.createExpressionStatement(expr); } // 12.5 If statement function parseIfStatement() { - var test, consequent, alternate; + var test, consequent, alternate; - expectKeyword('if'); + expectKeyword('if'); - expect('('); + expect('('); - test = parseExpression(); + test = parseExpression(); - expect(')'); + expect(')'); - consequent = parseStatement(); + consequent = parseStatement(); - if (matchKeyword('else')) { - lex(); - alternate = parseStatement(); - } else { - alternate = null; - } + if (matchKeyword('else')) { + lex(); + alternate = parseStatement(); + } else { + alternate = null; + } - return delegate.createIfStatement(test, consequent, alternate); + return delegate.createIfStatement(test, consequent, alternate); } // 12.6 Iteration Statements function parseDoWhileStatement() { - var body, test, oldInIteration; + var body, test, oldInIteration; - expectKeyword('do'); + expectKeyword('do'); - oldInIteration = state.inIteration; - state.inIteration = true; + oldInIteration = state.inIteration; + state.inIteration = true; - body = parseStatement(); + body = parseStatement(); - state.inIteration = oldInIteration; + state.inIteration = oldInIteration; - expectKeyword('while'); + expectKeyword('while'); - expect('('); + expect('('); - test = parseExpression(); + test = parseExpression(); - expect(')'); + expect(')'); - if (match(';')) { - lex(); - } + if (match(';')) { + lex(); + } - return delegate.createDoWhileStatement(body, test); + return delegate.createDoWhileStatement(body, test); } function parseWhileStatement() { - var test, body, oldInIteration; + var test, body, oldInIteration; - expectKeyword('while'); + expectKeyword('while'); - expect('('); + expect('('); - test = parseExpression(); + test = parseExpression(); - expect(')'); + expect(')'); - oldInIteration = state.inIteration; - state.inIteration = true; + oldInIteration = state.inIteration; + state.inIteration = true; - body = parseStatement(); + body = parseStatement(); - state.inIteration = oldInIteration; + state.inIteration = oldInIteration; - return delegate.createWhileStatement(test, body); + return delegate.createWhileStatement(test, body); } function parseForVariableDeclaration() { - var token, declarations, startToken; + var token, declarations, startToken; - startToken = lookahead; - token = lex(); - declarations = parseVariableDeclarationList(); + startToken = lookahead; + token = lex(); + declarations = parseVariableDeclarationList(); - return delegate.markEnd(delegate.createVariableDeclaration(declarations, token.value), startToken); + return delegate.markEnd( + delegate.createVariableDeclaration(declarations, token.value), + startToken + ); } function parseForStatement() { - var init, test, update, left, right, body, oldInIteration; + var init, test, update, left, right, body, oldInIteration; - init = test = update = null; + init = test = update = null; - expectKeyword('for'); + expectKeyword('for'); - expect('('); + expect('('); - if (match(';')) { + if (match(';')) { + lex(); + } else { + if (matchKeyword('var') || matchKeyword('let')) { + state.allowIn = false; + init = parseForVariableDeclaration(); + state.allowIn = true; + + if (init.declarations.length === 1 && matchKeyword('in')) { lex(); + left = init; + right = parseExpression(); + init = null; + } } else { - if (matchKeyword('var') || matchKeyword('let')) { - state.allowIn = false; - init = parseForVariableDeclaration(); - state.allowIn = true; + state.allowIn = false; + init = parseExpression(); + state.allowIn = true; - if (init.declarations.length === 1 && matchKeyword('in')) { - lex(); - left = init; - right = parseExpression(); - init = null; - } - } else { - state.allowIn = false; - init = parseExpression(); - state.allowIn = true; - - if (matchKeyword('in')) { - // LeftHandSideExpression - if (!isLeftHandSide(init)) { - throwErrorTolerant({}, Messages.InvalidLHSInForIn); - } - - lex(); - left = init; - right = parseExpression(); - init = null; - } + if (matchKeyword('in')) { + // LeftHandSideExpression + if (!isLeftHandSide(init)) { + throwErrorTolerant({}, Messages.InvalidLHSInForIn); } - if (typeof left === 'undefined') { - expect(';'); - } + lex(); + left = init; + right = parseExpression(); + init = null; + } } if (typeof left === 'undefined') { - - if (!match(';')) { - test = parseExpression(); - } - expect(';'); - - if (!match(')')) { - update = parseExpression(); - } + expect(';'); } + } - expect(')'); + if (typeof left === 'undefined') { + if (!match(';')) { + test = parseExpression(); + } + expect(';'); - oldInIteration = state.inIteration; - state.inIteration = true; + if (!match(')')) { + update = parseExpression(); + } + } - body = parseStatement(); + expect(')'); - state.inIteration = oldInIteration; + oldInIteration = state.inIteration; + state.inIteration = true; - return (typeof left === 'undefined') ? - delegate.createForStatement(init, test, update, body) : - delegate.createForInStatement(left, right, body); + body = parseStatement(); + + state.inIteration = oldInIteration; + + return typeof left === 'undefined' + ? delegate.createForStatement(init, test, update, body) + : delegate.createForInStatement(left, right, body); } // 12.7 The continue statement function parseContinueStatement() { - var label = null, key; + var label = null, + key; - expectKeyword('continue'); + expectKeyword('continue'); - // Optimize the most common form: 'continue;'. - if (source.charCodeAt(index) === 0x3B) { - lex(); + // Optimize the most common form: 'continue;'. + if (source.charCodeAt(index) === 0x3b) { + lex(); - if (!state.inIteration) { - throwError({}, Messages.IllegalContinue); - } - - return delegate.createContinueStatement(null); + if (!state.inIteration) { + throwError({}, Messages.IllegalContinue); } - if (peekLineTerminator()) { - if (!state.inIteration) { - throwError({}, Messages.IllegalContinue); - } + return delegate.createContinueStatement(null); + } - return delegate.createContinueStatement(null); + if (peekLineTerminator()) { + if (!state.inIteration) { + throwError({}, Messages.IllegalContinue); } - if (lookahead.type === Token.Identifier) { - label = parseVariableIdentifier(); + return delegate.createContinueStatement(null); + } - key = '$' + label.name; - if (!Object.prototype.hasOwnProperty.call(state.labelSet, key)) { - throwError({}, Messages.UnknownLabel, label.name); - } + if (lookahead.type === Token.Identifier) { + label = parseVariableIdentifier(); + + key = '$' + label.name; + if (!Object.prototype.hasOwnProperty.call(state.labelSet, key)) { + throwError({}, Messages.UnknownLabel, label.name); } + } - consumeSemicolon(); + consumeSemicolon(); - if (label === null && !state.inIteration) { - throwError({}, Messages.IllegalContinue); - } + if (label === null && !state.inIteration) { + throwError({}, Messages.IllegalContinue); + } - return delegate.createContinueStatement(label); + return delegate.createContinueStatement(label); } // 12.8 The break statement function parseBreakStatement() { - var label = null, key; + var label = null, + key; - expectKeyword('break'); + expectKeyword('break'); - // Catch the very common case first: immediately a semicolon (U+003B). - if (source.charCodeAt(index) === 0x3B) { - lex(); + // Catch the very common case first: immediately a semicolon (U+003B). + if (source.charCodeAt(index) === 0x3b) { + lex(); - if (!(state.inIteration || state.inSwitch)) { - throwError({}, Messages.IllegalBreak); - } - - return delegate.createBreakStatement(null); + if (!(state.inIteration || state.inSwitch)) { + throwError({}, Messages.IllegalBreak); } - if (peekLineTerminator()) { - if (!(state.inIteration || state.inSwitch)) { - throwError({}, Messages.IllegalBreak); - } + return delegate.createBreakStatement(null); + } - return delegate.createBreakStatement(null); + if (peekLineTerminator()) { + if (!(state.inIteration || state.inSwitch)) { + throwError({}, Messages.IllegalBreak); } - if (lookahead.type === Token.Identifier) { - label = parseVariableIdentifier(); + return delegate.createBreakStatement(null); + } - key = '$' + label.name; - if (!Object.prototype.hasOwnProperty.call(state.labelSet, key)) { - throwError({}, Messages.UnknownLabel, label.name); - } + if (lookahead.type === Token.Identifier) { + label = parseVariableIdentifier(); + + key = '$' + label.name; + if (!Object.prototype.hasOwnProperty.call(state.labelSet, key)) { + throwError({}, Messages.UnknownLabel, label.name); } + } - consumeSemicolon(); + consumeSemicolon(); - if (label === null && !(state.inIteration || state.inSwitch)) { - throwError({}, Messages.IllegalBreak); - } + if (label === null && !(state.inIteration || state.inSwitch)) { + throwError({}, Messages.IllegalBreak); + } - return delegate.createBreakStatement(label); + return delegate.createBreakStatement(label); } // 12.9 The return statement function parseReturnStatement() { - var argument = null; + var argument = null; - expectKeyword('return'); + expectKeyword('return'); - if (!state.inFunctionBody) { - throwErrorTolerant({}, Messages.IllegalReturn); + if (!state.inFunctionBody) { + throwErrorTolerant({}, Messages.IllegalReturn); + } + + // 'return' followed by a space and an identifier is very common. + if (source.charCodeAt(index) === 0x20) { + if (isIdentifierStart(source.charCodeAt(index + 1))) { + argument = parseExpression(); + consumeSemicolon(); + return delegate.createReturnStatement(argument); } + } - // 'return' followed by a space and an identifier is very common. - if (source.charCodeAt(index) === 0x20) { - if (isIdentifierStart(source.charCodeAt(index + 1))) { - argument = parseExpression(); - consumeSemicolon(); - return delegate.createReturnStatement(argument); - } + if (peekLineTerminator()) { + return delegate.createReturnStatement(null); + } + + if (!match(';')) { + if (!match('}') && lookahead.type !== Token.EOF) { + argument = parseExpression(); } + } - if (peekLineTerminator()) { - return delegate.createReturnStatement(null); - } + consumeSemicolon(); - if (!match(';')) { - if (!match('}') && lookahead.type !== Token.EOF) { - argument = parseExpression(); - } - } - - consumeSemicolon(); - - return delegate.createReturnStatement(argument); + return delegate.createReturnStatement(argument); } // 12.10 The with statement function parseWithStatement() { - var object, body; + var object, body; - if (strict) { - // TODO(ikarienator): Should we update the test cases instead? - skipComment(); - throwErrorTolerant({}, Messages.StrictModeWith); - } + if (strict) { + // TODO(ikarienator): Should we update the test cases instead? + skipComment(); + throwErrorTolerant({}, Messages.StrictModeWith); + } - expectKeyword('with'); + expectKeyword('with'); - expect('('); + expect('('); - object = parseExpression(); + object = parseExpression(); - expect(')'); + expect(')'); - body = parseStatement(); + body = parseStatement(); - return delegate.createWithStatement(object, body); + return delegate.createWithStatement(object, body); } // 12.10 The swith statement function parseSwitchCase() { - var test, consequent = [], statement, startToken; + var test, + consequent = [], + statement, + startToken; - startToken = lookahead; - if (matchKeyword('default')) { - lex(); - test = null; - } else { - expectKeyword('case'); - test = parseExpression(); + startToken = lookahead; + if (matchKeyword('default')) { + lex(); + test = null; + } else { + expectKeyword('case'); + test = parseExpression(); + } + expect(':'); + + while (index < length) { + if (match('}') || matchKeyword('default') || matchKeyword('case')) { + break; } - expect(':'); + statement = parseStatement(); + consequent.push(statement); + } - while (index < length) { - if (match('}') || matchKeyword('default') || matchKeyword('case')) { - break; - } - statement = parseStatement(); - consequent.push(statement); - } - - return delegate.markEnd(delegate.createSwitchCase(test, consequent), startToken); + return delegate.markEnd( + delegate.createSwitchCase(test, consequent), + startToken + ); } function parseSwitchStatement() { - var discriminant, cases, clause, oldInSwitch, defaultFound; + var discriminant, cases, clause, oldInSwitch, defaultFound; - expectKeyword('switch'); + expectKeyword('switch'); - expect('('); + expect('('); - discriminant = parseExpression(); + discriminant = parseExpression(); - expect(')'); + expect(')'); - expect('{'); + expect('{'); - cases = []; - - if (match('}')) { - lex(); - return delegate.createSwitchStatement(discriminant, cases); - } - - oldInSwitch = state.inSwitch; - state.inSwitch = true; - defaultFound = false; - - while (index < length) { - if (match('}')) { - break; - } - clause = parseSwitchCase(); - if (clause.test === null) { - if (defaultFound) { - throwError({}, Messages.MultipleDefaultsInSwitch); - } - defaultFound = true; - } - cases.push(clause); - } - - state.inSwitch = oldInSwitch; - - expect('}'); + cases = []; + if (match('}')) { + lex(); return delegate.createSwitchStatement(discriminant, cases); + } + + oldInSwitch = state.inSwitch; + state.inSwitch = true; + defaultFound = false; + + while (index < length) { + if (match('}')) { + break; + } + clause = parseSwitchCase(); + if (clause.test === null) { + if (defaultFound) { + throwError({}, Messages.MultipleDefaultsInSwitch); + } + defaultFound = true; + } + cases.push(clause); + } + + state.inSwitch = oldInSwitch; + + expect('}'); + + return delegate.createSwitchStatement(discriminant, cases); } // 12.13 The throw statement function parseThrowStatement() { - var argument; + var argument; - expectKeyword('throw'); + expectKeyword('throw'); - if (peekLineTerminator()) { - throwError({}, Messages.NewlineAfterThrow); - } + if (peekLineTerminator()) { + throwError({}, Messages.NewlineAfterThrow); + } - argument = parseExpression(); + argument = parseExpression(); - consumeSemicolon(); + consumeSemicolon(); - return delegate.createThrowStatement(argument); + return delegate.createThrowStatement(argument); } // 12.14 The try statement function parseCatchClause() { - var param, body, startToken; + var param, body, startToken; - startToken = lookahead; - expectKeyword('catch'); + startToken = lookahead; + expectKeyword('catch'); - expect('('); - if (match(')')) { - throwUnexpected(lookahead); - } + expect('('); + if (match(')')) { + throwUnexpected(lookahead); + } - param = parseVariableIdentifier(); - // 12.14.1 - if (strict && isRestrictedWord(param.name)) { - throwErrorTolerant({}, Messages.StrictCatchVariable); - } + param = parseVariableIdentifier(); + // 12.14.1 + if (strict && isRestrictedWord(param.name)) { + throwErrorTolerant({}, Messages.StrictCatchVariable); + } - expect(')'); - body = parseBlock(); - return delegate.markEnd(delegate.createCatchClause(param, body), startToken); + expect(')'); + body = parseBlock(); + return delegate.markEnd( + delegate.createCatchClause(param, body), + startToken + ); } function parseTryStatement() { - var block, handlers = [], finalizer = null; + var block, + handlers = [], + finalizer = null; - expectKeyword('try'); + expectKeyword('try'); - block = parseBlock(); + block = parseBlock(); - if (matchKeyword('catch')) { - handlers.push(parseCatchClause()); - } + if (matchKeyword('catch')) { + handlers.push(parseCatchClause()); + } - if (matchKeyword('finally')) { - lex(); - finalizer = parseBlock(); - } + if (matchKeyword('finally')) { + lex(); + finalizer = parseBlock(); + } - if (handlers.length === 0 && !finalizer) { - throwError({}, Messages.NoCatchOrFinally); - } + if (handlers.length === 0 && !finalizer) { + throwError({}, Messages.NoCatchOrFinally); + } - return delegate.createTryStatement(block, [], handlers, finalizer); + return delegate.createTryStatement(block, [], handlers, finalizer); } // 12.15 The debugger statement function parseDebuggerStatement() { - expectKeyword('debugger'); + expectKeyword('debugger'); - consumeSemicolon(); + consumeSemicolon(); - return delegate.createDebuggerStatement(); + return delegate.createDebuggerStatement(); } // 12 Statements function parseStatement() { - var type = lookahead.type, - expr, - labeledBody, - key, - startToken; + var type = lookahead.type, + expr, + labeledBody, + key, + startToken; - if (type === Token.EOF) { - throwUnexpected(lookahead); + if (type === Token.EOF) { + throwUnexpected(lookahead); + } + + if (type === Token.Punctuator && lookahead.value === '{') { + return parseBlock(); + } + + startToken = lookahead; + + if (type === Token.Punctuator) { + switch (lookahead.value) { + case ';': + return delegate.markEnd(parseEmptyStatement(), startToken); + case '(': + return delegate.markEnd(parseExpressionStatement(), startToken); + default: + break; + } + } + + if (type === Token.Keyword) { + switch (lookahead.value) { + case 'break': + return delegate.markEnd(parseBreakStatement(), startToken); + case 'continue': + return delegate.markEnd(parseContinueStatement(), startToken); + case 'debugger': + return delegate.markEnd(parseDebuggerStatement(), startToken); + case 'do': + return delegate.markEnd(parseDoWhileStatement(), startToken); + case 'for': + return delegate.markEnd(parseForStatement(), startToken); + case 'function': + return delegate.markEnd(parseFunctionDeclaration(), startToken); + case 'if': + return delegate.markEnd(parseIfStatement(), startToken); + case 'return': + return delegate.markEnd(parseReturnStatement(), startToken); + case 'switch': + return delegate.markEnd(parseSwitchStatement(), startToken); + case 'throw': + return delegate.markEnd(parseThrowStatement(), startToken); + case 'try': + return delegate.markEnd(parseTryStatement(), startToken); + case 'var': + return delegate.markEnd(parseVariableStatement(), startToken); + case 'while': + return delegate.markEnd(parseWhileStatement(), startToken); + case 'with': + return delegate.markEnd(parseWithStatement(), startToken); + default: + break; + } + } + + expr = parseExpression(); + + // 12.12 Labelled Statements + if (expr.type === Syntax.Identifier && match(':')) { + lex(); + + key = '$' + expr.name; + if (Object.prototype.hasOwnProperty.call(state.labelSet, key)) { + throwError({}, Messages.Redeclaration, 'Label', expr.name); } - if (type === Token.Punctuator && lookahead.value === '{') { - return parseBlock(); - } + state.labelSet[key] = true; + labeledBody = parseStatement(); + delete state.labelSet[key]; + return delegate.markEnd( + delegate.createLabeledStatement(expr, labeledBody), + startToken + ); + } - startToken = lookahead; + consumeSemicolon(); - if (type === Token.Punctuator) { - switch (lookahead.value) { - case ';': - return delegate.markEnd(parseEmptyStatement(), startToken); - case '(': - return delegate.markEnd(parseExpressionStatement(), startToken); - default: - break; - } - } - - if (type === Token.Keyword) { - switch (lookahead.value) { - case 'break': - return delegate.markEnd(parseBreakStatement(), startToken); - case 'continue': - return delegate.markEnd(parseContinueStatement(), startToken); - case 'debugger': - return delegate.markEnd(parseDebuggerStatement(), startToken); - case 'do': - return delegate.markEnd(parseDoWhileStatement(), startToken); - case 'for': - return delegate.markEnd(parseForStatement(), startToken); - case 'function': - return delegate.markEnd(parseFunctionDeclaration(), startToken); - case 'if': - return delegate.markEnd(parseIfStatement(), startToken); - case 'return': - return delegate.markEnd(parseReturnStatement(), startToken); - case 'switch': - return delegate.markEnd(parseSwitchStatement(), startToken); - case 'throw': - return delegate.markEnd(parseThrowStatement(), startToken); - case 'try': - return delegate.markEnd(parseTryStatement(), startToken); - case 'var': - return delegate.markEnd(parseVariableStatement(), startToken); - case 'while': - return delegate.markEnd(parseWhileStatement(), startToken); - case 'with': - return delegate.markEnd(parseWithStatement(), startToken); - default: - break; - } - } - - expr = parseExpression(); - - // 12.12 Labelled Statements - if ((expr.type === Syntax.Identifier) && match(':')) { - lex(); - - key = '$' + expr.name; - if (Object.prototype.hasOwnProperty.call(state.labelSet, key)) { - throwError({}, Messages.Redeclaration, 'Label', expr.name); - } - - state.labelSet[key] = true; - labeledBody = parseStatement(); - delete state.labelSet[key]; - return delegate.markEnd(delegate.createLabeledStatement(expr, labeledBody), startToken); - } - - consumeSemicolon(); - - return delegate.markEnd(delegate.createExpressionStatement(expr), startToken); + return delegate.markEnd( + delegate.createExpressionStatement(expr), + startToken + ); } // 13 Function Definition function parseFunctionSourceElements() { - var sourceElement, sourceElements = [], token, directive, firstRestricted, - oldLabelSet, oldInIteration, oldInSwitch, oldInFunctionBody, startToken; + var sourceElement, + sourceElements = [], + token, + directive, + firstRestricted, + oldLabelSet, + oldInIteration, + oldInSwitch, + oldInFunctionBody, + startToken; - startToken = lookahead; - expect('{'); + startToken = lookahead; + expect('{'); - while (index < length) { - if (lookahead.type !== Token.StringLiteral) { - break; - } - token = lookahead; - - sourceElement = parseSourceElement(); - sourceElements.push(sourceElement); - if (sourceElement.expression.type !== Syntax.Literal) { - // this is not directive - break; - } - directive = source.slice(token.start + 1, token.end - 1); - if (directive === 'use strict') { - strict = true; - if (firstRestricted) { - throwErrorTolerant(firstRestricted, Messages.StrictOctalLiteral); - } - } else { - if (!firstRestricted && token.octal) { - firstRestricted = token; - } - } + while (index < length) { + if (lookahead.type !== Token.StringLiteral) { + break; } + token = lookahead; - oldLabelSet = state.labelSet; - oldInIteration = state.inIteration; - oldInSwitch = state.inSwitch; - oldInFunctionBody = state.inFunctionBody; - - state.labelSet = {}; - state.inIteration = false; - state.inSwitch = false; - state.inFunctionBody = true; - - while (index < length) { - if (match('}')) { - break; - } - sourceElement = parseSourceElement(); - if (typeof sourceElement === 'undefined') { - break; - } - sourceElements.push(sourceElement); + sourceElement = parseSourceElement(); + sourceElements.push(sourceElement); + if (sourceElement.expression.type !== Syntax.Literal) { + // this is not directive + break; } + directive = source.slice(token.start + 1, token.end - 1); + if (directive === 'use strict') { + strict = true; + if (firstRestricted) { + throwErrorTolerant(firstRestricted, Messages.StrictOctalLiteral); + } + } else { + if (!firstRestricted && token.octal) { + firstRestricted = token; + } + } + } - expect('}'); + oldLabelSet = state.labelSet; + oldInIteration = state.inIteration; + oldInSwitch = state.inSwitch; + oldInFunctionBody = state.inFunctionBody; - state.labelSet = oldLabelSet; - state.inIteration = oldInIteration; - state.inSwitch = oldInSwitch; - state.inFunctionBody = oldInFunctionBody; + state.labelSet = {}; + state.inIteration = false; + state.inSwitch = false; + state.inFunctionBody = true; - return delegate.markEnd(delegate.createBlockStatement(sourceElements), startToken); + while (index < length) { + if (match('}')) { + break; + } + sourceElement = parseSourceElement(); + if (typeof sourceElement === 'undefined') { + break; + } + sourceElements.push(sourceElement); + } + + expect('}'); + + state.labelSet = oldLabelSet; + state.inIteration = oldInIteration; + state.inSwitch = oldInSwitch; + state.inFunctionBody = oldInFunctionBody; + + return delegate.markEnd( + delegate.createBlockStatement(sourceElements), + startToken + ); } function parseParams(firstRestricted) { - var param, params = [], token, stricted, paramSet, key, message; - expect('('); + var param, + params = [], + token, + stricted, + paramSet, + key, + message; + expect('('); - if (!match(')')) { - paramSet = {}; - while (index < length) { - token = lookahead; - param = parseVariableIdentifier(); - key = '$' + token.value; - if (strict) { - if (isRestrictedWord(token.value)) { - stricted = token; - message = Messages.StrictParamName; - } - if (Object.prototype.hasOwnProperty.call(paramSet, key)) { - stricted = token; - message = Messages.StrictParamDupe; - } - } else if (!firstRestricted) { - if (isRestrictedWord(token.value)) { - firstRestricted = token; - message = Messages.StrictParamName; - } else if (isStrictModeReservedWord(token.value)) { - firstRestricted = token; - message = Messages.StrictReservedWord; - } else if (Object.prototype.hasOwnProperty.call(paramSet, key)) { - firstRestricted = token; - message = Messages.StrictParamDupe; - } - } - params.push(param); - paramSet[key] = true; - if (match(')')) { - break; - } - expect(','); + if (!match(')')) { + paramSet = {}; + while (index < length) { + token = lookahead; + param = parseVariableIdentifier(); + key = '$' + token.value; + if (strict) { + if (isRestrictedWord(token.value)) { + stricted = token; + message = Messages.StrictParamName; } + if (Object.prototype.hasOwnProperty.call(paramSet, key)) { + stricted = token; + message = Messages.StrictParamDupe; + } + } else if (!firstRestricted) { + if (isRestrictedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictParamName; + } else if (isStrictModeReservedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictReservedWord; + } else if (Object.prototype.hasOwnProperty.call(paramSet, key)) { + firstRestricted = token; + message = Messages.StrictParamDupe; + } + } + params.push(param); + paramSet[key] = true; + if (match(')')) { + break; + } + expect(','); } + } - expect(')'); + expect(')'); - return { - params: params, - stricted: stricted, - firstRestricted: firstRestricted, - message: message - }; + return { + params: params, + stricted: stricted, + firstRestricted: firstRestricted, + message: message, + }; } function parseFunctionDeclaration() { - var id, params = [], body, token, stricted, tmp, firstRestricted, message, previousStrict, startToken; + var id, + params = [], + body, + token, + stricted, + tmp, + firstRestricted, + message, + previousStrict, + startToken; - startToken = lookahead; + startToken = lookahead; - expectKeyword('function'); - token = lookahead; - id = parseVariableIdentifier(); - if (strict) { - if (isRestrictedWord(token.value)) { - throwErrorTolerant(token, Messages.StrictFunctionName); - } - } else { - if (isRestrictedWord(token.value)) { - firstRestricted = token; - message = Messages.StrictFunctionName; - } else if (isStrictModeReservedWord(token.value)) { - firstRestricted = token; - message = Messages.StrictReservedWord; - } + expectKeyword('function'); + token = lookahead; + id = parseVariableIdentifier(); + if (strict) { + if (isRestrictedWord(token.value)) { + throwErrorTolerant(token, Messages.StrictFunctionName); } - - tmp = parseParams(firstRestricted); - params = tmp.params; - stricted = tmp.stricted; - firstRestricted = tmp.firstRestricted; - if (tmp.message) { - message = tmp.message; + } else { + if (isRestrictedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictFunctionName; + } else if (isStrictModeReservedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictReservedWord; } + } - previousStrict = strict; - body = parseFunctionSourceElements(); - if (strict && firstRestricted) { - throwError(firstRestricted, message); - } - if (strict && stricted) { - throwErrorTolerant(stricted, message); - } - strict = previousStrict; + tmp = parseParams(firstRestricted); + params = tmp.params; + stricted = tmp.stricted; + firstRestricted = tmp.firstRestricted; + if (tmp.message) { + message = tmp.message; + } - return delegate.markEnd(delegate.createFunctionDeclaration(id, params, [], body), startToken); + previousStrict = strict; + body = parseFunctionSourceElements(); + if (strict && firstRestricted) { + throwError(firstRestricted, message); + } + if (strict && stricted) { + throwErrorTolerant(stricted, message); + } + strict = previousStrict; + + return delegate.markEnd( + delegate.createFunctionDeclaration(id, params, [], body), + startToken + ); } function parseFunctionExpression() { - var token, id = null, stricted, firstRestricted, message, tmp, params = [], body, previousStrict, startToken; + var token, + id = null, + stricted, + firstRestricted, + message, + tmp, + params = [], + body, + previousStrict, + startToken; - startToken = lookahead; - expectKeyword('function'); + startToken = lookahead; + expectKeyword('function'); - if (!match('(')) { - token = lookahead; - id = parseVariableIdentifier(); - if (strict) { - if (isRestrictedWord(token.value)) { - throwErrorTolerant(token, Messages.StrictFunctionName); - } - } else { - if (isRestrictedWord(token.value)) { - firstRestricted = token; - message = Messages.StrictFunctionName; - } else if (isStrictModeReservedWord(token.value)) { - firstRestricted = token; - message = Messages.StrictReservedWord; - } - } + if (!match('(')) { + token = lookahead; + id = parseVariableIdentifier(); + if (strict) { + if (isRestrictedWord(token.value)) { + throwErrorTolerant(token, Messages.StrictFunctionName); + } + } else { + if (isRestrictedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictFunctionName; + } else if (isStrictModeReservedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictReservedWord; + } } + } - tmp = parseParams(firstRestricted); - params = tmp.params; - stricted = tmp.stricted; - firstRestricted = tmp.firstRestricted; - if (tmp.message) { - message = tmp.message; - } + tmp = parseParams(firstRestricted); + params = tmp.params; + stricted = tmp.stricted; + firstRestricted = tmp.firstRestricted; + if (tmp.message) { + message = tmp.message; + } - previousStrict = strict; - body = parseFunctionSourceElements(); - if (strict && firstRestricted) { - throwError(firstRestricted, message); - } - if (strict && stricted) { - throwErrorTolerant(stricted, message); - } - strict = previousStrict; + previousStrict = strict; + body = parseFunctionSourceElements(); + if (strict && firstRestricted) { + throwError(firstRestricted, message); + } + if (strict && stricted) { + throwErrorTolerant(stricted, message); + } + strict = previousStrict; - return delegate.markEnd(delegate.createFunctionExpression(id, params, [], body), startToken); + return delegate.markEnd( + delegate.createFunctionExpression(id, params, [], body), + startToken + ); } // 14 Program function parseSourceElement() { - if (lookahead.type === Token.Keyword) { - switch (lookahead.value) { - case 'const': - case 'let': - return parseConstLetDeclaration(lookahead.value); - case 'function': - return parseFunctionDeclaration(); - default: - return parseStatement(); - } - } - - if (lookahead.type !== Token.EOF) { + if (lookahead.type === Token.Keyword) { + switch (lookahead.value) { + case 'const': + case 'let': + return parseConstLetDeclaration(lookahead.value); + case 'function': + return parseFunctionDeclaration(); + default: return parseStatement(); } + } + + if (lookahead.type !== Token.EOF) { + return parseStatement(); + } } function parseSourceElements() { - var sourceElement, sourceElements = [], token, directive, firstRestricted; + var sourceElement, + sourceElements = [], + token, + directive, + firstRestricted; - while (index < length) { - token = lookahead; - if (token.type !== Token.StringLiteral) { - break; - } - - sourceElement = parseSourceElement(); - sourceElements.push(sourceElement); - if (sourceElement.expression.type !== Syntax.Literal) { - // this is not directive - break; - } - directive = source.slice(token.start + 1, token.end - 1); - if (directive === 'use strict') { - strict = true; - if (firstRestricted) { - throwErrorTolerant(firstRestricted, Messages.StrictOctalLiteral); - } - } else { - if (!firstRestricted && token.octal) { - firstRestricted = token; - } - } + while (index < length) { + token = lookahead; + if (token.type !== Token.StringLiteral) { + break; } - while (index < length) { - sourceElement = parseSourceElement(); - /* istanbul ignore if */ - if (typeof sourceElement === 'undefined') { - break; - } - sourceElements.push(sourceElement); + sourceElement = parseSourceElement(); + sourceElements.push(sourceElement); + if (sourceElement.expression.type !== Syntax.Literal) { + // this is not directive + break; } - return sourceElements; + directive = source.slice(token.start + 1, token.end - 1); + if (directive === 'use strict') { + strict = true; + if (firstRestricted) { + throwErrorTolerant(firstRestricted, Messages.StrictOctalLiteral); + } + } else { + if (!firstRestricted && token.octal) { + firstRestricted = token; + } + } + } + + while (index < length) { + sourceElement = parseSourceElement(); + /* istanbul ignore if */ + if (typeof sourceElement === 'undefined') { + break; + } + sourceElements.push(sourceElement); + } + return sourceElements; } function parseProgram() { - var body, startToken; + var body, startToken; - skipComment(); - peek(); - startToken = lookahead; - strict = false; + skipComment(); + peek(); + startToken = lookahead; + strict = false; - body = parseSourceElements(); - return delegate.markEnd(delegate.createProgram(body), startToken); + body = parseSourceElements(); + return delegate.markEnd(delegate.createProgram(body), startToken); } function filterTokenLocation() { - var i, entry, token, tokens = []; + var i, + entry, + token, + tokens = []; - for (i = 0; i < extra.tokens.length; ++i) { - entry = extra.tokens[i]; - token = { - type: entry.type, - value: entry.value - }; - if (extra.range) { - token.range = entry.range; - } - if (extra.loc) { - token.loc = entry.loc; - } - tokens.push(token); + for (i = 0; i < extra.tokens.length; ++i) { + entry = extra.tokens[i]; + token = { + type: entry.type, + value: entry.value, + }; + if (extra.range) { + token.range = entry.range; } + if (extra.loc) { + token.loc = entry.loc; + } + tokens.push(token); + } - extra.tokens = tokens; + extra.tokens = tokens; } function tokenize(code, options) { - var toString, - token, - tokens; + var toString, token, tokens; - toString = String; - if (typeof code !== 'string' && !(code instanceof String)) { - code = toString(code); + toString = String; + if (typeof code !== 'string' && !(code instanceof String)) { + code = toString(code); + } + + delegate = SyntaxTreeDelegate; + source = code; + index = 0; + lineNumber = source.length > 0 ? 1 : 0; + lineStart = 0; + length = source.length; + lookahead = null; + state = { + allowIn: true, + labelSet: {}, + inFunctionBody: false, + inIteration: false, + inSwitch: false, + lastCommentStart: -1, + }; + + extra = {}; + + // Options matching. + options = options || {}; + + // Of course we collect tokens here. + options.tokens = true; + extra.tokens = []; + extra.tokenize = true; + // The following two fields are necessary to compute the Regex tokens. + extra.openParenToken = -1; + extra.openCurlyToken = -1; + + extra.range = typeof options.range === 'boolean' && options.range; + extra.loc = typeof options.loc === 'boolean' && options.loc; + + if (typeof options.comment === 'boolean' && options.comment) { + extra.comments = []; + } + if (typeof options.tolerant === 'boolean' && options.tolerant) { + extra.errors = []; + } + + try { + peek(); + if (lookahead.type === Token.EOF) { + return extra.tokens; } - delegate = SyntaxTreeDelegate; - source = code; - index = 0; - lineNumber = (source.length > 0) ? 1 : 0; - lineStart = 0; - length = source.length; - lookahead = null; - state = { - allowIn: true, - labelSet: {}, - inFunctionBody: false, - inIteration: false, - inSwitch: false, - lastCommentStart: -1 - }; - - extra = {}; - - // Options matching. - options = options || {}; - - // Of course we collect tokens here. - options.tokens = true; - extra.tokens = []; - extra.tokenize = true; - // The following two fields are necessary to compute the Regex tokens. - extra.openParenToken = -1; - extra.openCurlyToken = -1; - - extra.range = (typeof options.range === 'boolean') && options.range; - extra.loc = (typeof options.loc === 'boolean') && options.loc; - - if (typeof options.comment === 'boolean' && options.comment) { - extra.comments = []; - } - if (typeof options.tolerant === 'boolean' && options.tolerant) { - extra.errors = []; - } - - try { - peek(); - if (lookahead.type === Token.EOF) { - return extra.tokens; - } - + token = lex(); + while (lookahead.type !== Token.EOF) { + try { token = lex(); - while (lookahead.type !== Token.EOF) { - try { - token = lex(); - } catch (lexError) { - token = lookahead; - if (extra.errors) { - extra.errors.push(lexError); - // We have to break on the first error - // to avoid infinite loops. - break; - } else { - throw lexError; - } - } + } catch (lexError) { + token = lookahead; + if (extra.errors) { + extra.errors.push(lexError); + // We have to break on the first error + // to avoid infinite loops. + break; + } else { + throw lexError; } - - filterTokenLocation(); - tokens = extra.tokens; - if (typeof extra.comments !== 'undefined') { - tokens.comments = extra.comments; - } - if (typeof extra.errors !== 'undefined') { - tokens.errors = extra.errors; - } - } catch (e) { - throw e; - } finally { - extra = {}; + } } - return tokens; + + filterTokenLocation(); + tokens = extra.tokens; + if (typeof extra.comments !== 'undefined') { + tokens.comments = extra.comments; + } + if (typeof extra.errors !== 'undefined') { + tokens.errors = extra.errors; + } + } catch (e) { + throw e; + } finally { + extra = {}; + } + return tokens; } function parse(code, options) { - var program, toString; + var program, toString; - toString = String; - if (typeof code !== 'string' && !(code instanceof String)) { - code = toString(code); + toString = String; + if (typeof code !== 'string' && !(code instanceof String)) { + code = toString(code); + } + + delegate = SyntaxTreeDelegate; + source = code; + index = 0; + lineNumber = source.length > 0 ? 1 : 0; + lineStart = 0; + length = source.length; + lookahead = null; + state = { + allowIn: true, + labelSet: {}, + inFunctionBody: false, + inIteration: false, + inSwitch: false, + lastCommentStart: -1, + }; + + extra = {}; + if (typeof options !== 'undefined') { + extra.range = typeof options.range === 'boolean' && options.range; + extra.loc = typeof options.loc === 'boolean' && options.loc; + extra.attachComment = + typeof options.attachComment === 'boolean' && options.attachComment; + + if ( + extra.loc && + options.source !== null && + options.source !== undefined + ) { + extra.source = toString(options.source); } - delegate = SyntaxTreeDelegate; - source = code; - index = 0; - lineNumber = (source.length > 0) ? 1 : 0; - lineStart = 0; - length = source.length; - lookahead = null; - state = { - allowIn: true, - labelSet: {}, - inFunctionBody: false, - inIteration: false, - inSwitch: false, - lastCommentStart: -1 - }; + if (typeof options.tokens === 'boolean' && options.tokens) { + extra.tokens = []; + } + if (typeof options.comment === 'boolean' && options.comment) { + extra.comments = []; + } + if (typeof options.tolerant === 'boolean' && options.tolerant) { + extra.errors = []; + } + if (extra.attachComment) { + extra.range = true; + extra.comments = []; + extra.bottomRightStack = []; + extra.trailingComments = []; + extra.leadingComments = []; + } + } + try { + program = parseProgram(); + if (typeof extra.comments !== 'undefined') { + program.comments = extra.comments; + } + if (typeof extra.tokens !== 'undefined') { + filterTokenLocation(); + program.tokens = extra.tokens; + } + if (typeof extra.errors !== 'undefined') { + program.errors = extra.errors; + } + } catch (e) { + throw e; + } finally { extra = {}; - if (typeof options !== 'undefined') { - extra.range = (typeof options.range === 'boolean') && options.range; - extra.loc = (typeof options.loc === 'boolean') && options.loc; - extra.attachComment = (typeof options.attachComment === 'boolean') && options.attachComment; + } - if (extra.loc && options.source !== null && options.source !== undefined) { - extra.source = toString(options.source); - } - - if (typeof options.tokens === 'boolean' && options.tokens) { - extra.tokens = []; - } - if (typeof options.comment === 'boolean' && options.comment) { - extra.comments = []; - } - if (typeof options.tolerant === 'boolean' && options.tolerant) { - extra.errors = []; - } - if (extra.attachComment) { - extra.range = true; - extra.comments = []; - extra.bottomRightStack = []; - extra.trailingComments = []; - extra.leadingComments = []; - } - } - - try { - program = parseProgram(); - if (typeof extra.comments !== 'undefined') { - program.comments = extra.comments; - } - if (typeof extra.tokens !== 'undefined') { - filterTokenLocation(); - program.tokens = extra.tokens; - } - if (typeof extra.errors !== 'undefined') { - program.errors = extra.errors; - } - } catch (e) { - throw e; - } finally { - extra = {}; - } - - return program; + return program; } // Sync with *.json manifests. @@ -3734,1517 +4091,1906 @@ parseStatement: true, parseSourceElement: true */ exports.parse = parse; // Deep copy. - /* istanbul ignore next */ - exports.Syntax = (function () { - var name, types = {}; + /* istanbul ignore next */ + exports.Syntax = (function() { + var name, + types = {}; - if (typeof Object.create === 'function') { - types = Object.create(null); + if (typeof Object.create === 'function') { + types = Object.create(null); + } + + for (name in Syntax) { + if (Syntax.hasOwnProperty(name)) { + types[name] = Syntax[name]; } + } - for (name in Syntax) { - if (Syntax.hasOwnProperty(name)) { - types[name] = Syntax[name]; - } - } - - if (typeof Object.freeze === 'function') { - Object.freeze(types); - } - - return types; - }()); - -})); -/* vim: set sw=4 ts=4 et tw=80 : */ + if (typeof Object.freeze === 'function') { + Object.freeze(types); + } + return types; + })(); + }); + /* vim: set sw=4 ts=4 et tw=80 : */ })(null); /*! * falafel (c) James Halliday / MIT License * https://github.com/substack/node-falafel */ -(function(require,module){ -var parse = require('esprima').parse; -var objectKeys = Object.keys || function (obj) { - var keys = []; - for (var key in obj) keys.push(key); - return keys; -}; -var forEach = function (xs, fn) { +(function(require, module) { + var parse = require('esprima').parse; + var objectKeys = + Object.keys || + function(obj) { + var keys = []; + for (var key in obj) keys.push(key); + return keys; + }; + var forEach = function(xs, fn) { if (xs.forEach) return xs.forEach(fn); for (var i = 0; i < xs.length; i++) { - fn.call(xs, xs[i], i, xs); + fn.call(xs, xs[i], i, xs); } -}; + }; -var isArray = Array.isArray || function (xs) { - return Object.prototype.toString.call(xs) === '[object Array]'; -}; + var isArray = + Array.isArray || + function(xs) { + return Object.prototype.toString.call(xs) === '[object Array]'; + }; -module.exports = function (src, opts, fn) { + module.exports = function(src, opts, fn) { if (typeof opts === 'function') { - fn = opts; - opts = {}; + fn = opts; + opts = {}; } if (typeof src === 'object') { - opts = src; - src = opts.source; - delete opts.source; + opts = src; + src = opts.source; + delete opts.source; } src = src === undefined ? opts.source : src; opts.range = true; if (typeof src !== 'string') src = String(src); - + var ast = parse(src, opts); - + var result = { - chunks : src.split(''), - toString : function () { return result.chunks.join('') }, - inspect : function () { return result.toString() } + chunks: src.split(''), + toString: function() { + return result.chunks.join(''); + }, + inspect: function() { + return result.toString(); + }, }; var index = 0; - - (function walk (node, parent) { - insertHelpers(node, parent, result.chunks); - - forEach(objectKeys(node), function (key) { - if (key === 'parent') return; - - var child = node[key]; - if (isArray(child)) { - forEach(child, function (c) { - if (c && typeof c.type === 'string') { - walk(c, node); - } - }); + + (function walk(node, parent) { + insertHelpers(node, parent, result.chunks); + + forEach(objectKeys(node), function(key) { + if (key === 'parent') return; + + var child = node[key]; + if (isArray(child)) { + forEach(child, function(c) { + if (c && typeof c.type === 'string') { + walk(c, node); } - else if (child && typeof child.type === 'string') { - insertHelpers(child, node, result.chunks); - walk(child, node); - } - }); - fn(node); - })(ast, undefined); - - return result; -}; - -function insertHelpers (node, parent, chunks) { - if (!node.range) return; - - node.parent = parent; - - node.source = function () { - return chunks.slice( - node.range[0], node.range[1] - ).join(''); - }; - - if (node.update && typeof node.update === 'object') { - var prev = node.update; - forEach(objectKeys(prev), function (key) { - update[key] = prev[key]; - }); - node.update = update; - } - else { - node.update = update; - } - - function update (s) { - chunks[node.range[0]] = s; - for (var i = node.range[0] + 1; i < node.range[1]; i++) { - chunks[i] = ''; + }); + } else if (child && typeof child.type === 'string') { + insertHelpers(child, node, result.chunks); + walk(child, node); } + }); + fn(node); + })(ast, undefined); + + return result; + }; + + function insertHelpers(node, parent, chunks) { + if (!node.range) return; + + node.parent = parent; + + node.source = function() { + return chunks.slice(node.range[0], node.range[1]).join(''); }; -} -window.falafel = module.exports;})(function(){return {parse: esprima.parse};},{exports: {}}); + if (node.update && typeof node.update === 'object') { + var prev = node.update; + forEach(objectKeys(prev), function(key) { + update[key] = prev[key]; + }); + node.update = update; + } else { + node.update = update; + } + + function update(s) { + chunks[node.range[0]] = s; + for (var i = node.range[0] + 1; i < node.range[1]; i++) { + chunks[i] = ''; + } + } + } + + window.falafel = module.exports; +})( + function() { + return { parse: esprima.parse }; + }, + { exports: {} } +); var inBrowser = typeof window !== 'undefined' && this === window; -var parseAndModify = (inBrowser ? window.falafel : require("falafel")); +var parseAndModify = inBrowser ? window.falafel : require('falafel'); -(inBrowser ? window : exports).blanket = (function(){ - var linesToAddTracking = [ - "ExpressionStatement", - "BreakStatement" , - "ContinueStatement" , - "VariableDeclaration", - "ReturnStatement" , - "ThrowStatement" , - "TryStatement" , - "FunctionDeclaration" , - "IfStatement" , - "WhileStatement" , - "DoWhileStatement" , - "ForStatement" , - "ForInStatement" , - "SwitchStatement" , - "WithStatement" +(inBrowser ? window : exports).blanket = (function() { + var linesToAddTracking = [ + 'ExpressionStatement', + 'BreakStatement', + 'ContinueStatement', + 'VariableDeclaration', + 'ReturnStatement', + 'ThrowStatement', + 'TryStatement', + 'FunctionDeclaration', + 'IfStatement', + 'WhileStatement', + 'DoWhileStatement', + 'ForStatement', + 'ForInStatement', + 'SwitchStatement', + 'WithStatement', ], linesToAddBrackets = [ - "IfStatement" , - "WhileStatement" , - "DoWhileStatement" , - "ForStatement" , - "ForInStatement" , - "WithStatement" + 'IfStatement', + 'WhileStatement', + 'DoWhileStatement', + 'ForStatement', + 'ForInStatement', + 'WithStatement', ], __blanket, - copynumber = Math.floor(Math.random()*1000), - coverageInfo = {},options = { - reporter: null, - adapter:null, - filter: null, - customVariable: null, - loader: null, - ignoreScriptError: false, - existingRequireJS:false, - autoStart: false, - timeout: 180, - ignoreCors: false, - branchTracking: false, - sourceURL: false, - debug:false, - engineOnly:false, - testReadyCallback:null, - commonJS:false, - instrumentCache:false, - modulePattern: null + copynumber = Math.floor(Math.random() * 1000), + coverageInfo = {}, + options = { + reporter: null, + adapter: null, + filter: null, + customVariable: null, + loader: null, + ignoreScriptError: false, + existingRequireJS: false, + autoStart: false, + timeout: 180, + ignoreCors: false, + branchTracking: false, + sourceURL: false, + debug: false, + engineOnly: false, + testReadyCallback: null, + commonJS: false, + instrumentCache: false, + modulePattern: null, }; - - if (inBrowser && typeof window.blanket !== 'undefined'){ - __blanket = window.blanket.noConflict(); - } - - _blanket = { - noConflict: function(){ - if (__blanket){ - return __blanket; - } - return _blanket; - }, - _getCopyNumber: function(){ - //internal method - //for differentiating between instances - return copynumber; - }, - extend: function(obj) { - //borrowed from underscore - _blanket._extend(_blanket,obj); - }, - _extend: function(dest,source){ - if (source) { - for (var prop in source) { - if ( dest[prop] instanceof Object && typeof dest[prop] !== "function"){ - _blanket._extend(dest[prop],source[prop]); - }else{ - dest[prop] = source[prop]; - } - } + + if (inBrowser && typeof window.blanket !== 'undefined') { + __blanket = window.blanket.noConflict(); + } + + _blanket = { + noConflict: function() { + if (__blanket) { + return __blanket; + } + return _blanket; + }, + _getCopyNumber: function() { + //internal method + //for differentiating between instances + return copynumber; + }, + extend: function(obj) { + //borrowed from underscore + _blanket._extend(_blanket, obj); + }, + _extend: function(dest, source) { + if (source) { + for (var prop in source) { + if ( + dest[prop] instanceof Object && + typeof dest[prop] !== 'function' + ) { + _blanket._extend(dest[prop], source[prop]); + } else { + dest[prop] = source[prop]; } - }, - getCovVar: function(){ - var opt = _blanket.options("customVariable"); - if (opt){ - if (_blanket.options("debug")) {console.log("BLANKET-Using custom tracking variable:",opt);} - return inBrowser ? "window."+opt : opt; - } - return inBrowser ? "window._$blanket" : "_$jscoverage"; - }, - options: function(key,value){ - if (typeof key !== "string"){ - _blanket._extend(options,key); - }else if (typeof value === 'undefined'){ - return options[key]; - }else{ - options[key]=value; - } - }, - instrument: function(config, next){ - //check instrumented hash table, - //return instrumented code if available. - var inFile = config.inputFile, - inFileName = config.inputFileName; - //check instrument cache - if (_blanket.options("instrumentCache") && sessionStorage && sessionStorage.getItem("blanket_instrument_store-"+inFileName)){ - if (_blanket.options("debug")) {console.log("BLANKET-Reading instrumentation from cache: ",inFileName);} - next(sessionStorage.getItem("blanket_instrument_store-"+inFileName)); - }else{ - var sourceArray = _blanket._prepareSource(inFile); - _blanket._trackingArraySetup=[]; - //remove shebang - inFile = inFile.replace(/^\#\!.*/, ""); - var instrumented = parseAndModify(inFile,{loc:true,comment:true}, _blanket._addTracking(inFileName)); - instrumented = _blanket._trackingSetup(inFileName,sourceArray)+instrumented; - if (_blanket.options("sourceURL")){ - instrumented += "\n//@ sourceURL="+inFileName.replace("http://",""); - } - if (_blanket.options("debug")) {console.log("BLANKET-Instrumented file: ",inFileName);} - if (_blanket.options("instrumentCache") && sessionStorage){ - if (_blanket.options("debug")) {console.log("BLANKET-Saving instrumentation to cache: ",inFileName);} - sessionStorage.setItem("blanket_instrument_store-"+inFileName,instrumented); - } - next(instrumented); - } - }, - _trackingArraySetup: [], - _branchingArraySetup: [], - _prepareSource: function(source){ - return source.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/(\r\n|\n|\r)/gm,"\n").split('\n'); - }, - _trackingSetup: function(filename,sourceArray){ - var branches = _blanket.options("branchTracking"); - var sourceString = sourceArray.join("',\n'"); - var intro = ""; - var covVar = _blanket.getCovVar(); - - intro += "if (typeof "+covVar+" === 'undefined') "+covVar+" = {};\n"; - if (branches){ - intro += "var _$branchFcn=function(f,l,c,r){ "; - intro += "if (!!r) { "; - intro += covVar+"[f].branchData[l][c][0] = "+covVar+"[f].branchData[l][c][0] || [];"; - intro += covVar+"[f].branchData[l][c][0].push(r); }"; - intro += "else { "; - intro += covVar+"[f].branchData[l][c][1] = "+covVar+"[f].branchData[l][c][1] || [];"; - intro += covVar+"[f].branchData[l][c][1].push(r); }"; - intro += "return r;};\n"; - } - intro += "if (typeof "+covVar+"['"+filename+"'] === 'undefined'){"; - - intro += covVar+"['"+filename+"']=[];\n"; - if (branches){ - intro += covVar+"['"+filename+"'].branchData=[];\n"; - } - intro += covVar+"['"+filename+"'].source=['"+sourceString+"'];\n"; - //initialize array values - _blanket._trackingArraySetup.sort(function(a,b){ - return parseInt(a,10) > parseInt(b,10); - }).forEach(function(item){ - intro += covVar+"['"+filename+"']["+item+"]=0;\n"; - }); - if (branches){ - _blanket._branchingArraySetup.sort(function(a,b){ - return a.line > b.line; - }).sort(function(a,b){ - return a.column > b.column; - }).forEach(function(item){ - if (item.file === filename){ - intro += "if (typeof "+ covVar+"['"+filename+"'].branchData["+item.line+"] === 'undefined'){\n"; - intro += covVar+"['"+filename+"'].branchData["+item.line+"]=[];\n"; - intro += "}"; - intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"] = [];\n"; - intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"].consequent = "+JSON.stringify(item.consequent)+";\n"; - intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"].alternate = "+JSON.stringify(item.alternate)+";\n"; - } - }); - } - intro += "}"; - - return intro; - }, - _blockifyIf: function(node){ - if (linesToAddBrackets.indexOf(node.type) > -1){ - var bracketsExistObject = node.consequent || node.body; - var bracketsExistAlt = node.alternate; - if( bracketsExistAlt && bracketsExistAlt.type !== "BlockStatement") { - bracketsExistAlt.update("{\n"+bracketsExistAlt.source()+"}\n"); - } - if( bracketsExistObject && bracketsExistObject.type !== "BlockStatement") { - bracketsExistObject.update("{\n"+bracketsExistObject.source()+"}\n"); - } - } - }, - _trackBranch: function(node,filename){ - //recursive on consequent and alternative - var line = node.loc.start.line; - var col = node.loc.start.column; - - _blanket._branchingArraySetup.push({ - line: line, - column: col, - file:filename, - consequent: node.consequent.loc, - alternate: node.alternate.loc - }); - - var updated = "_$branchFcn"+ - "('"+filename+"',"+line+","+col+","+node.test.source()+ - ")?"+node.consequent.source()+":"+node.alternate.source(); - node.update(updated); - }, - _addTracking: function (filename) { - //falafel doesn't take a file name - //so we include the filename in a closure - //and return the function to falafel - var covVar = _blanket.getCovVar(); - - return function(node){ - _blanket._blockifyIf(node); - - if (linesToAddTracking.indexOf(node.type) > -1 && node.parent.type !== "LabeledStatement") { - _blanket._checkDefs(node,filename); - if (node.type === "VariableDeclaration" && - (node.parent.type === "ForStatement" || node.parent.type === "ForInStatement")){ - return; - } - if (node.loc && node.loc.start){ - node.update(covVar+"['"+filename+"']["+node.loc.start.line+"]++;\n"+node.source()); - _blanket._trackingArraySetup.push(node.loc.start.line); - }else{ - //I don't think we can handle a node with no location - throw new Error("The instrumenter encountered a node with no location: "+Object.keys(node)); - } - }else if (_blanket.options("branchTracking") && node.type === "ConditionalExpression"){ - _blanket._trackBranch(node,filename); - } - }; - }, - _checkDefs: function(node,filename){ - // Make sure developers don't redefine window. if they do, inform them it is wrong. - if (inBrowser){ - if (node.type === "VariableDeclaration" && node.declarations) { - node.declarations.forEach(function(declaration) { - if (declaration.id.name === "window") { - throw new Error("Instrumentation error, you cannot redefine the 'window' variable in " + filename + ":" + node.loc.start.line); - } - }); - } - if (node.type === "FunctionDeclaration" && node.params) { - node.params.forEach(function(param) { - if (param.name === "window") { - throw new Error("Instrumentation error, you cannot redefine the 'window' variable in " + filename + ":" + node.loc.start.line); - } - }); - } - //Make sure developers don't redefine the coverage variable - if (node.type === "ExpressionStatement" && - node.expression && node.expression.left && - node.expression.left.object && node.expression.left.property && - node.expression.left.object.name + - "." + node.expression.left.property.name === _blanket.getCovVar()) { - throw new Error("Instrumentation error, you cannot redefine the coverage variable in " + filename + ":" + node.loc.start.line); - } - }else{ - //Make sure developers don't redefine the coverage variable in node - if (node.type === "ExpressionStatement" && - node.expression && node.expression.left && - !node.expression.left.object && !node.expression.left.property && - node.expression.left.name === _blanket.getCovVar()) { - throw new Error("Instrumentation error, you cannot redefine the coverage variable in " + filename + ":" + node.loc.start.line); - } - } - }, - setupCoverage: function(){ - coverageInfo.instrumentation = "blanket"; - coverageInfo.stats = { - "suites": 0, - "tests": 0, - "passes": 0, - "pending": 0, - "failures": 0, - "start": new Date() - }; - }, - _checkIfSetup: function(){ - if (!coverageInfo.stats){ - throw new Error("You must call blanket.setupCoverage() first."); - } - }, - onTestStart: function(){ - if (_blanket.options("debug")) {console.log("BLANKET-Test event started");} - this._checkIfSetup(); - coverageInfo.stats.tests++; - coverageInfo.stats.pending++; - }, - onTestDone: function(total,passed){ - this._checkIfSetup(); - if(passed === total){ - coverageInfo.stats.passes++; - }else{ - coverageInfo.stats.failures++; - } - coverageInfo.stats.pending--; - }, - onModuleStart: function(){ - this._checkIfSetup(); - coverageInfo.stats.suites++; - }, - onTestsDone: function(){ - if (_blanket.options("debug")) {console.log("BLANKET-Test event done");} - this._checkIfSetup(); - coverageInfo.stats.end = new Date(); - - if (inBrowser){ - this.report(coverageInfo); - }else{ - if (!_blanket.options("branchTracking")){ - delete (inBrowser ? window : global)[_blanket.getCovVar()].branchFcn; - } - this.options("reporter").call(this,coverageInfo); - } } - }; - return _blanket; + } + }, + getCovVar: function() { + var opt = _blanket.options('customVariable'); + if (opt) { + if (_blanket.options('debug')) { + console.log('BLANKET-Using custom tracking variable:', opt); + } + return inBrowser ? 'window.' + opt : opt; + } + return inBrowser ? 'window._$blanket' : '_$jscoverage'; + }, + options: function(key, value) { + if (typeof key !== 'string') { + _blanket._extend(options, key); + } else if (typeof value === 'undefined') { + return options[key]; + } else { + options[key] = value; + } + }, + instrument: function(config, next) { + //check instrumented hash table, + //return instrumented code if available. + var inFile = config.inputFile, + inFileName = config.inputFileName; + //check instrument cache + if ( + _blanket.options('instrumentCache') && + sessionStorage && + sessionStorage.getItem('blanket_instrument_store-' + inFileName) + ) { + if (_blanket.options('debug')) { + console.log( + 'BLANKET-Reading instrumentation from cache: ', + inFileName + ); + } + next(sessionStorage.getItem('blanket_instrument_store-' + inFileName)); + } else { + var sourceArray = _blanket._prepareSource(inFile); + _blanket._trackingArraySetup = []; + //remove shebang + inFile = inFile.replace(/^\#\!.*/, ''); + var instrumented = parseAndModify( + inFile, + { loc: true, comment: true }, + _blanket._addTracking(inFileName) + ); + instrumented = + _blanket._trackingSetup(inFileName, sourceArray) + instrumented; + if (_blanket.options('sourceURL')) { + instrumented += + '\n//@ sourceURL=' + inFileName.replace('http://', ''); + } + if (_blanket.options('debug')) { + console.log('BLANKET-Instrumented file: ', inFileName); + } + if (_blanket.options('instrumentCache') && sessionStorage) { + if (_blanket.options('debug')) { + console.log( + 'BLANKET-Saving instrumentation to cache: ', + inFileName + ); + } + sessionStorage.setItem( + 'blanket_instrument_store-' + inFileName, + instrumented + ); + } + next(instrumented); + } + }, + _trackingArraySetup: [], + _branchingArraySetup: [], + _prepareSource: function(source) { + return source + .replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(/(\r\n|\n|\r)/gm, '\n') + .split('\n'); + }, + _trackingSetup: function(filename, sourceArray) { + var branches = _blanket.options('branchTracking'); + var sourceString = sourceArray.join("',\n'"); + var intro = ''; + var covVar = _blanket.getCovVar(); + + intro += + 'if (typeof ' + covVar + " === 'undefined') " + covVar + ' = {};\n'; + if (branches) { + intro += 'var _$branchFcn=function(f,l,c,r){ '; + intro += 'if (!!r) { '; + intro += + covVar + + '[f].branchData[l][c][0] = ' + + covVar + + '[f].branchData[l][c][0] || [];'; + intro += covVar + '[f].branchData[l][c][0].push(r); }'; + intro += 'else { '; + intro += + covVar + + '[f].branchData[l][c][1] = ' + + covVar + + '[f].branchData[l][c][1] || [];'; + intro += covVar + '[f].branchData[l][c][1].push(r); }'; + intro += 'return r;};\n'; + } + intro += + 'if (typeof ' + covVar + "['" + filename + "'] === 'undefined'){"; + + intro += covVar + "['" + filename + "']=[];\n"; + if (branches) { + intro += covVar + "['" + filename + "'].branchData=[];\n"; + } + intro += + covVar + "['" + filename + "'].source=['" + sourceString + "'];\n"; + //initialize array values + _blanket._trackingArraySetup + .sort(function(a, b) { + return parseInt(a, 10) > parseInt(b, 10); + }) + .forEach(function(item) { + intro += covVar + "['" + filename + "'][" + item + ']=0;\n'; + }); + if (branches) { + _blanket._branchingArraySetup + .sort(function(a, b) { + return a.line > b.line; + }) + .sort(function(a, b) { + return a.column > b.column; + }) + .forEach(function(item) { + if (item.file === filename) { + intro += + 'if (typeof ' + + covVar + + "['" + + filename + + "'].branchData[" + + item.line + + "] === 'undefined'){\n"; + intro += + covVar + + "['" + + filename + + "'].branchData[" + + item.line + + ']=[];\n'; + intro += '}'; + intro += + covVar + + "['" + + filename + + "'].branchData[" + + item.line + + '][' + + item.column + + '] = [];\n'; + intro += + covVar + + "['" + + filename + + "'].branchData[" + + item.line + + '][' + + item.column + + '].consequent = ' + + JSON.stringify(item.consequent) + + ';\n'; + intro += + covVar + + "['" + + filename + + "'].branchData[" + + item.line + + '][' + + item.column + + '].alternate = ' + + JSON.stringify(item.alternate) + + ';\n'; + } + }); + } + intro += '}'; + + return intro; + }, + _blockifyIf: function(node) { + if (linesToAddBrackets.indexOf(node.type) > -1) { + var bracketsExistObject = node.consequent || node.body; + var bracketsExistAlt = node.alternate; + if (bracketsExistAlt && bracketsExistAlt.type !== 'BlockStatement') { + bracketsExistAlt.update('{\n' + bracketsExistAlt.source() + '}\n'); + } + if ( + bracketsExistObject && + bracketsExistObject.type !== 'BlockStatement' + ) { + bracketsExistObject.update( + '{\n' + bracketsExistObject.source() + '}\n' + ); + } + } + }, + _trackBranch: function(node, filename) { + //recursive on consequent and alternative + var line = node.loc.start.line; + var col = node.loc.start.column; + + _blanket._branchingArraySetup.push({ + line: line, + column: col, + file: filename, + consequent: node.consequent.loc, + alternate: node.alternate.loc, + }); + + var updated = + '_$branchFcn' + + "('" + + filename + + "'," + + line + + ',' + + col + + ',' + + node.test.source() + + ')?' + + node.consequent.source() + + ':' + + node.alternate.source(); + node.update(updated); + }, + _addTracking: function(filename) { + //falafel doesn't take a file name + //so we include the filename in a closure + //and return the function to falafel + var covVar = _blanket.getCovVar(); + + return function(node) { + _blanket._blockifyIf(node); + + if ( + linesToAddTracking.indexOf(node.type) > -1 && + node.parent.type !== 'LabeledStatement' + ) { + _blanket._checkDefs(node, filename); + if ( + node.type === 'VariableDeclaration' && + (node.parent.type === 'ForStatement' || + node.parent.type === 'ForInStatement') + ) { + return; + } + if (node.loc && node.loc.start) { + node.update( + covVar + + "['" + + filename + + "'][" + + node.loc.start.line + + ']++;\n' + + node.source() + ); + _blanket._trackingArraySetup.push(node.loc.start.line); + } else { + //I don't think we can handle a node with no location + throw new Error( + 'The instrumenter encountered a node with no location: ' + + Object.keys(node) + ); + } + } else if ( + _blanket.options('branchTracking') && + node.type === 'ConditionalExpression' + ) { + _blanket._trackBranch(node, filename); + } + }; + }, + _checkDefs: function(node, filename) { + // Make sure developers don't redefine window. if they do, inform them it is wrong. + if (inBrowser) { + if (node.type === 'VariableDeclaration' && node.declarations) { + node.declarations.forEach(function(declaration) { + if (declaration.id.name === 'window') { + throw new Error( + "Instrumentation error, you cannot redefine the 'window' variable in " + + filename + + ':' + + node.loc.start.line + ); + } + }); + } + if (node.type === 'FunctionDeclaration' && node.params) { + node.params.forEach(function(param) { + if (param.name === 'window') { + throw new Error( + "Instrumentation error, you cannot redefine the 'window' variable in " + + filename + + ':' + + node.loc.start.line + ); + } + }); + } + //Make sure developers don't redefine the coverage variable + if ( + node.type === 'ExpressionStatement' && + node.expression && + node.expression.left && + node.expression.left.object && + node.expression.left.property && + node.expression.left.object.name + + '.' + + node.expression.left.property.name === + _blanket.getCovVar() + ) { + throw new Error( + 'Instrumentation error, you cannot redefine the coverage variable in ' + + filename + + ':' + + node.loc.start.line + ); + } + } else { + //Make sure developers don't redefine the coverage variable in node + if ( + node.type === 'ExpressionStatement' && + node.expression && + node.expression.left && + !node.expression.left.object && + !node.expression.left.property && + node.expression.left.name === _blanket.getCovVar() + ) { + throw new Error( + 'Instrumentation error, you cannot redefine the coverage variable in ' + + filename + + ':' + + node.loc.start.line + ); + } + } + }, + setupCoverage: function() { + coverageInfo.instrumentation = 'blanket'; + coverageInfo.stats = { + suites: 0, + tests: 0, + passes: 0, + pending: 0, + failures: 0, + start: new Date(), + }; + }, + _checkIfSetup: function() { + if (!coverageInfo.stats) { + throw new Error('You must call blanket.setupCoverage() first.'); + } + }, + onTestStart: function() { + if (_blanket.options('debug')) { + console.log('BLANKET-Test event started'); + } + this._checkIfSetup(); + coverageInfo.stats.tests++; + coverageInfo.stats.pending++; + }, + onTestDone: function(total, passed) { + this._checkIfSetup(); + if (passed === total) { + coverageInfo.stats.passes++; + } else { + coverageInfo.stats.failures++; + } + coverageInfo.stats.pending--; + }, + onModuleStart: function() { + this._checkIfSetup(); + coverageInfo.stats.suites++; + }, + onTestsDone: function() { + if (_blanket.options('debug')) { + console.log('BLANKET-Test event done'); + } + this._checkIfSetup(); + coverageInfo.stats.end = new Date(); + + if (inBrowser) { + this.report(coverageInfo); + } else { + if (!_blanket.options('branchTracking')) { + delete (inBrowser ? window : global)[_blanket.getCovVar()].branchFcn; + } + this.options('reporter').call(this, coverageInfo); + } + }, + }; + return _blanket; })(); -(function(_blanket){ - var oldOptions = _blanket.options; -_blanket.extend({ - outstandingRequireFiles:[], - options: function(key,value){ - var newVal={}; +(function(_blanket) { + var oldOptions = _blanket.options; + _blanket.extend({ + outstandingRequireFiles: [], + options: function(key, value) { + var newVal = {}; - if (typeof key !== "string"){ - //key is key/value map - oldOptions(key); - newVal = key; - }else if (typeof value === 'undefined'){ - //accessor - return oldOptions(key); - }else{ - //setter - oldOptions(key,value); - newVal[key] = value; - } + if (typeof key !== 'string') { + //key is key/value map + oldOptions(key); + newVal = key; + } else if (typeof value === 'undefined') { + //accessor + return oldOptions(key); + } else { + //setter + oldOptions(key, value); + newVal[key] = value; + } - if (newVal.adapter){ - _blanket._loadFile(newVal.adapter); - } - if (newVal.loader){ - _blanket._loadFile(newVal.loader); - } + if (newVal.adapter) { + _blanket._loadFile(newVal.adapter); + } + if (newVal.loader) { + _blanket._loadFile(newVal.loader); + } }, - requiringFile: function(filename,done){ - if (typeof filename === "undefined"){ - _blanket.outstandingRequireFiles=[]; - }else if (typeof done === "undefined"){ - _blanket.outstandingRequireFiles.push(filename); - }else{ - _blanket.outstandingRequireFiles.splice(_blanket.outstandingRequireFiles.indexOf(filename),1); - } + requiringFile: function(filename, done) { + if (typeof filename === 'undefined') { + _blanket.outstandingRequireFiles = []; + } else if (typeof done === 'undefined') { + _blanket.outstandingRequireFiles.push(filename); + } else { + _blanket.outstandingRequireFiles.splice( + _blanket.outstandingRequireFiles.indexOf(filename), + 1 + ); + } }, - requireFilesLoaded: function(){ - return _blanket.outstandingRequireFiles.length === 0; + requireFilesLoaded: function() { + return _blanket.outstandingRequireFiles.length === 0; }, - showManualLoader: function(){ - if (document.getElementById("blanketLoaderDialog")){ - return; - } - //copied from http://blog.avtex.com/2012/01/26/cross-browser-css-only-modal-box/ - var loader = "
"; - loader += " 
"; - loader += "
"; - loader += "
"; - loader += "Error: Blanket.js encountered a cross origin request error while instrumenting the source files. "; - loader += "

This is likely caused by the source files being referenced locally (using the file:// protocol). "; - loader += "

Some solutions include
starting Chrome with special flags, running a server locally, or using a browser without these CORS restrictions (Safari)."; - loader += "
"; - if (typeof FileReader !== "undefined"){ - loader += "
Or, try the experimental loader. When prompted, simply click on the directory containing all the source files you want covered."; - loader += "Start Loader"; - loader += ""; - } - loader += "
Close"; - loader += "
"; - loader += "
"; + showManualLoader: function() { + if (document.getElementById('blanketLoaderDialog')) { + return; + } + //copied from http://blog.avtex.com/2012/01/26/cross-browser-css-only-modal-box/ + var loader = "
"; + loader += ' 
'; + loader += "
"; + loader += "
"; + loader += + 'Error: Blanket.js encountered a cross origin request error while instrumenting the source files. '; + loader += + '

This is likely caused by the source files being referenced locally (using the file:// protocol). '; + loader += + "

Some solutions include starting Chrome with special flags, running a server locally, or using a browser without these CORS restrictions (Safari)."; + loader += '
'; + if (typeof FileReader !== 'undefined') { + loader += + '
Or, try the experimental loader. When prompted, simply click on the directory containing all the source files you want covered.'; + loader += + 'Start Loader'; + loader += + ""; + } + loader += + "
Close"; + loader += "
"; + loader += '
'; - var css = ".blanketDialogWrapper {"; - css += "display:block;"; - css += "position:fixed;"; - css += "z-index:40001; }"; + var css = '.blanketDialogWrapper {'; + css += 'display:block;'; + css += 'position:fixed;'; + css += 'z-index:40001; }'; - css += ".blanketDialogOverlay {"; - css += "position:fixed;"; - css += "width:100%;"; - css += "height:100%;"; - css += "background-color:black;"; - css += "opacity:.5; "; - css += "-ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=50)'; "; - css += "filter:alpha(opacity=50); "; - css += "z-index:40001; }"; + css += '.blanketDialogOverlay {'; + css += 'position:fixed;'; + css += 'width:100%;'; + css += 'height:100%;'; + css += 'background-color:black;'; + css += 'opacity:.5; '; + css += + "-ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=50)'; "; + css += 'filter:alpha(opacity=50); '; + css += 'z-index:40001; }'; - css += ".blanketDialogVerticalOffset { "; - css += "position:fixed;"; - css += "top:30%;"; - css += "width:100%;"; - css += "z-index:40002; }"; + css += '.blanketDialogVerticalOffset { '; + css += 'position:fixed;'; + css += 'top:30%;'; + css += 'width:100%;'; + css += 'z-index:40002; }'; - css += ".blanketDialogBox { "; - css += "width:405px; "; - css += "position:relative;"; - css += "margin:0 auto;"; - css += "background-color:white;"; - css += "padding:10px;"; - css += "border:1px solid black; }"; + css += '.blanketDialogBox { '; + css += 'width:405px; '; + css += 'position:relative;'; + css += 'margin:0 auto;'; + css += 'background-color:white;'; + css += 'padding:10px;'; + css += 'border:1px solid black; }'; - var dom = document.createElement("style"); - dom.innerHTML = css; - document.head.appendChild(dom); - - var div = document.createElement("div"); - div.id = "blanketLoaderDialog"; - div.className = "blanketDialogWrapper"; - div.innerHTML = loader; - document.body.insertBefore(div,document.body.firstChild); + var dom = document.createElement('style'); + dom.innerHTML = css; + document.head.appendChild(dom); + var div = document.createElement('div'); + div.id = 'blanketLoaderDialog'; + div.className = 'blanketDialogWrapper'; + div.innerHTML = loader; + document.body.insertBefore(div, document.body.firstChild); }, - manualFileLoader: function(files){ - var toArray =Array.prototype.slice; - files = toArray.call(files).filter(function(item){ - return item.type !== ""; - }); - var sessionLength = files.length-1; - var sessionIndx=0; - var sessionArray = {}; - if (sessionStorage["blanketSessionLoader"]){ - sessionArray = JSON.parse(sessionStorage["blanketSessionLoader"]); - } + manualFileLoader: function(files) { + var toArray = Array.prototype.slice; + files = toArray.call(files).filter(function(item) { + return item.type !== ''; + }); + var sessionLength = files.length - 1; + var sessionIndx = 0; + var sessionArray = {}; + if (sessionStorage['blanketSessionLoader']) { + sessionArray = JSON.parse(sessionStorage['blanketSessionLoader']); + } - - var fileLoader = function(event){ - var fileContent = event.currentTarget.result; - var file = files[sessionIndx]; - var filename = file.webkitRelativePath && file.webkitRelativePath !== '' ? file.webkitRelativePath : file.name; - sessionArray[filename] = fileContent; - sessionIndx++; - if (sessionIndx === sessionLength){ - sessionStorage.setItem("blanketSessionLoader", JSON.stringify(sessionArray)); - document.location.reload(); - }else{ - readFile(files[sessionIndx]); - } - }; - function readFile(file){ - var reader = new FileReader(); - reader.onload = fileLoader; - reader.readAsText(file); + var fileLoader = function(event) { + var fileContent = event.currentTarget.result; + var file = files[sessionIndx]; + var filename = + file.webkitRelativePath && file.webkitRelativePath !== '' + ? file.webkitRelativePath + : file.name; + sessionArray[filename] = fileContent; + sessionIndx++; + if (sessionIndx === sessionLength) { + sessionStorage.setItem( + 'blanketSessionLoader', + JSON.stringify(sessionArray) + ); + document.location.reload(); + } else { + readFile(files[sessionIndx]); } - readFile(files[sessionIndx]); + }; + function readFile(file) { + var reader = new FileReader(); + reader.onload = fileLoader; + reader.readAsText(file); + } + readFile(files[sessionIndx]); }, - _loadFile: function(path){ - if (typeof path !== "undefined"){ - var request = new XMLHttpRequest(); - request.open('GET', path, false); - request.send(); - _blanket._addScript(request.responseText); - } + _loadFile: function(path) { + if (typeof path !== 'undefined') { + var request = new XMLHttpRequest(); + request.open('GET', path, false); + request.send(); + _blanket._addScript(request.responseText); + } }, - _addScript: function(data){ - /*var script = document.createElement("script"); + _addScript: function(data) { + /*var script = document.createElement("script"); script.type = "text/javascript"; script.text = data; (document.body || document.getElementsByTagName('head')[0]).appendChild(script);*/ - (1,eval)(data); + (1, eval)(data); }, - hasAdapter: function(callback){ - return _blanket.options("adapter") !== null; + hasAdapter: function(callback) { + return _blanket.options('adapter') !== null; }, - report: function(coverage_data){ - if (!document.getElementById("blanketLoaderDialog")){ - //all found, clear it - _blanket.blanketSession = null; - } - coverage_data.files = window._$blanket; - var require = blanket.options("commonJS") ? blanket._commonjs.require : window.require; + report: function(coverage_data) { + if (!document.getElementById('blanketLoaderDialog')) { + //all found, clear it + _blanket.blanketSession = null; + } + coverage_data.files = window._$blanket; + var require = blanket.options('commonJS') + ? blanket._commonjs.require + : window.require; - // Check if we have any covered files that requires reporting - // otherwise just exit gracefully. - if (!coverage_data.files || !Object.keys(coverage_data.files).length) { - if (_blanket.options("debug")) {console.log("BLANKET-Reporting No files were instrumented.");} - return; + // Check if we have any covered files that requires reporting + // otherwise just exit gracefully. + if (!coverage_data.files || !Object.keys(coverage_data.files).length) { + if (_blanket.options('debug')) { + console.log('BLANKET-Reporting No files were instrumented.'); + } + return; + } + + if (typeof coverage_data.files.branchFcn !== 'undefined') { + delete coverage_data.files.branchFcn; + } + if (typeof _blanket.options('reporter') === 'string') { + _blanket._loadFile(_blanket.options('reporter')); + _blanket.customReporter( + coverage_data, + _blanket.options('reporter_options') + ); + } else if (typeof _blanket.options('reporter') === 'function') { + _blanket.options('reporter')( + coverage_data, + _blanket.options('reporter_options') + ); + } else if (typeof _blanket.defaultReporter === 'function') { + _blanket.defaultReporter( + coverage_data, + _blanket.options('reporter_options') + ); + } else { + throw new Error('no reporter defined.'); + } + }, + _bindStartTestRunner: function(bindEvent, startEvent) { + if (bindEvent) { + bindEvent(startEvent); + } else { + window.addEventListener('load', startEvent, false); + } + }, + _loadSourceFiles: function(callback) { + var require = blanket.options('commonJS') + ? blanket._commonjs.require + : window.require; + function copy(o) { + var _copy = Object.create(Object.getPrototypeOf(o)); + var propNames = Object.getOwnPropertyNames(o); + + propNames.forEach(function(name) { + var desc = Object.getOwnPropertyDescriptor(o, name); + Object.defineProperty(_copy, name, desc); + }); + + return _copy; + } + if (_blanket.options('debug')) { + console.log('BLANKET-Collecting page scripts'); + } + var scripts = _blanket.utils.collectPageScripts(); + //_blanket.options("filter",scripts); + if (scripts.length === 0) { + callback(); + } else { + //check session state + if (sessionStorage['blanketSessionLoader']) { + _blanket.blanketSession = JSON.parse( + sessionStorage['blanketSessionLoader'] + ); } - if (typeof coverage_data.files.branchFcn !== "undefined"){ - delete coverage_data.files.branchFcn; - } - if (typeof _blanket.options("reporter") === "string"){ - _blanket._loadFile(_blanket.options("reporter")); - _blanket.customReporter(coverage_data,_blanket.options("reporter_options")); - }else if (typeof _blanket.options("reporter") === "function"){ - _blanket.options("reporter")(coverage_data,_blanket.options("reporter_options")); - }else if (typeof _blanket.defaultReporter === 'function'){ - _blanket.defaultReporter(coverage_data,_blanket.options("reporter_options")); - }else{ - throw new Error("no reporter defined."); - } - }, - _bindStartTestRunner: function(bindEvent,startEvent){ - if (bindEvent){ - bindEvent(startEvent); - }else{ - window.addEventListener("load",startEvent,false); - } - }, - _loadSourceFiles: function(callback){ - var require = blanket.options("commonJS") ? blanket._commonjs.require : window.require; - function copy(o){ - var _copy = Object.create( Object.getPrototypeOf(o) ); - var propNames = Object.getOwnPropertyNames(o); + scripts.forEach(function(file, indx) { + _blanket.utils.cache[file] = { + loaded: false, + }; + }); - propNames.forEach(function(name){ - var desc = Object.getOwnPropertyDescriptor(o, name); - Object.defineProperty(_copy, name, desc); + var currScript = -1; + _blanket.utils.loadAll(function(test) { + if (test) { + return typeof scripts[currScript + 1] !== 'undefined'; + } + currScript++; + if (currScript >= scripts.length) { + return null; + } + return scripts[currScript]; + }, callback); + } + }, + beforeStartTestRunner: function(opts) { + opts = opts || {}; + opts.checkRequirejs = + typeof opts.checkRequirejs === 'undefined' ? true : opts.checkRequirejs; + opts.callback = opts.callback || function() {}; + opts.coverage = + typeof opts.coverage === 'undefined' ? true : opts.coverage; + if (opts.coverage) { + _blanket._bindStartTestRunner(opts.bindEvent, function() { + _blanket._loadSourceFiles(function() { + var allLoaded = function() { + return opts.condition + ? opts.condition() + : _blanket.requireFilesLoaded(); + }; + var check = function() { + if (allLoaded()) { + if (_blanket.options('debug')) { + console.log( + 'BLANKET-All files loaded, init start test runner callback.' + ); + } + var cb = _blanket.options('testReadyCallback'); + + if (cb) { + if (typeof cb === 'function') { + cb(opts.callback); + } else if (typeof cb === 'string') { + _blanket._addScript(cb); + opts.callback(); + } + } else { + opts.callback(); + } + } else { + setTimeout(check, 13); + } + }; + check(); }); - - return _copy; - } - if (_blanket.options("debug")) {console.log("BLANKET-Collecting page scripts");} - var scripts = _blanket.utils.collectPageScripts(); - //_blanket.options("filter",scripts); - if (scripts.length === 0){ - callback(); - }else{ - - //check session state - if (sessionStorage["blanketSessionLoader"]){ - _blanket.blanketSession = JSON.parse(sessionStorage["blanketSessionLoader"]); - } - - scripts.forEach(function(file,indx){ - _blanket.utils.cache[file]={ - loaded:false - }; - }); - - var currScript=-1; - _blanket.utils.loadAll(function(test){ - if (test){ - return typeof scripts[currScript+1] !== 'undefined'; - } - currScript++; - if (currScript >= scripts.length){ - return null; - } - return scripts[currScript]; - },callback); - } - }, - beforeStartTestRunner: function(opts){ - opts = opts || {}; - opts.checkRequirejs = typeof opts.checkRequirejs === "undefined" ? true : opts.checkRequirejs; - opts.callback = opts.callback || function() { }; - opts.coverage = typeof opts.coverage === "undefined" ? true : opts.coverage; - if (opts.coverage) { - _blanket._bindStartTestRunner(opts.bindEvent, - function(){ - _blanket._loadSourceFiles(function() { - - var allLoaded = function(){ - return opts.condition ? opts.condition() : _blanket.requireFilesLoaded(); - }; - var check = function() { - if (allLoaded()) { - if (_blanket.options("debug")) {console.log("BLANKET-All files loaded, init start test runner callback.");} - var cb = _blanket.options("testReadyCallback"); - - if (cb){ - if (typeof cb === "function"){ - cb(opts.callback); - }else if (typeof cb === "string"){ - _blanket._addScript(cb); - opts.callback(); - } - }else{ - opts.callback(); - } - } else { - setTimeout(check, 13); - } - }; - check(); - }); - }); - }else{ - opts.callback(); - } + }); + } else { + opts.callback(); + } }, utils: { - qualifyURL: function (url) { - //http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue - var a = document.createElement('a'); - a.href = url; - return a.href; - } - } -}); - + qualifyURL: function(url) { + //http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue + var a = document.createElement('a'); + a.href = url; + return a.href; + }, + }, + }); })(blanket); -blanket.defaultReporter = function(coverage){ - var cssSytle = "#blanket-main {margin:2px;background:#EEE;color:#333;clear:both;font-family:'Helvetica Neue Light', 'HelveticaNeue-Light', 'Helvetica Neue', Calibri, Helvetica, Arial, sans-serif; font-size:17px;} #blanket-main a {color:#333;text-decoration:none;} #blanket-main a:hover {text-decoration:underline;} .blanket {margin:0;padding:5px;clear:both;border-bottom: 1px solid #FFFFFF;} .bl-error {color:red;}.bl-success {color:#5E7D00;} .bl-file{width:auto;} .bl-cl{float:left;} .blanket div.rs {margin-left:50px; width:150px; float:right} .bl-nb {padding-right:10px;} #blanket-main a.bl-logo {color: #EB1764;cursor: pointer;font-weight: bold;text-decoration: none} .bl-source{ overflow-x:scroll; background-color: #FFFFFF; border: 1px solid #CBCBCB; color: #363636; margin: 25px 20px; width: 80%;} .bl-source div{white-space: pre;font-family: monospace;} .bl-source > div > span:first-child{background-color: #EAEAEA;color: #949494;display: inline-block;padding: 0 10px;text-align: center;width: 30px;} .bl-source .miss{background-color:#e6c3c7} .bl-source span.branchWarning{color:#000;background-color:yellow;} .bl-source span.branchOkay{color:#000;background-color:transparent;}", - successRate = 60, - head = document.head, - fileNumber = 0, - body = document.body, - headerContent, - hasBranchTracking = Object.keys(coverage.files).some(function(elem){ - return typeof coverage.files[elem].branchData !== 'undefined'; - }), - bodyContent = "
results
Coverage (%)
Covered/Total Smts.
"+(hasBranchTracking ? "
Covered/Total Branches
":"")+"
", - fileTemplate = "
{{fileNumber}}.{{file}}
{{percentage}} %
{{numberCovered}}/{{totalSmts}}
"+( hasBranchTracking ? "
{{passedBranches}}/{{totalBranches}}
" : "" )+"
"; - grandTotalTemplate = "
{{rowTitle}}
{{percentage}} %
{{numberCovered}}/{{totalSmts}}
"+( hasBranchTracking ? "
{{passedBranches}}/{{totalBranches}}
" : "" ) + "
"; +blanket.defaultReporter = function(coverage) { + var cssSytle = + "#blanket-main {margin:2px;background:#EEE;color:#333;clear:both;font-family:'Helvetica Neue Light', 'HelveticaNeue-Light', 'Helvetica Neue', Calibri, Helvetica, Arial, sans-serif; font-size:17px;} #blanket-main a {color:#333;text-decoration:none;} #blanket-main a:hover {text-decoration:underline;} .blanket {margin:0;padding:5px;clear:both;border-bottom: 1px solid #FFFFFF;} .bl-error {color:red;}.bl-success {color:#5E7D00;} .bl-file{width:auto;} .bl-cl{float:left;} .blanket div.rs {margin-left:50px; width:150px; float:right} .bl-nb {padding-right:10px;} #blanket-main a.bl-logo {color: #EB1764;cursor: pointer;font-weight: bold;text-decoration: none} .bl-source{ overflow-x:scroll; background-color: #FFFFFF; border: 1px solid #CBCBCB; color: #363636; margin: 25px 20px; width: 80%;} .bl-source div{white-space: pre;font-family: monospace;} .bl-source > div > span:first-child{background-color: #EAEAEA;color: #949494;display: inline-block;padding: 0 10px;text-align: center;width: 30px;} .bl-source .miss{background-color:#e6c3c7} .bl-source span.branchWarning{color:#000;background-color:yellow;} .bl-source span.branchOkay{color:#000;background-color:transparent;}", + successRate = 60, + head = document.head, + fileNumber = 0, + body = document.body, + headerContent, + hasBranchTracking = Object.keys(coverage.files).some(function(elem) { + return typeof coverage.files[elem].branchData !== 'undefined'; + }), + bodyContent = + "
results
Coverage (%)
Covered/Total Smts.
" + + (hasBranchTracking + ? "
Covered/Total Branches
" + : '') + + "
", + fileTemplate = + "
{{fileNumber}}.{{file}}
{{percentage}} %
{{numberCovered}}/{{totalSmts}}
" + + (hasBranchTracking + ? "
{{passedBranches}}/{{totalBranches}}
" + : '') + + "
"; + grandTotalTemplate = + "
{{rowTitle}}
{{percentage}} %
{{numberCovered}}/{{totalSmts}}
" + + (hasBranchTracking + ? "
{{passedBranches}}/{{totalBranches}}
" + : '') + + "
"; - function blanket_toggleSource(id) { - var element = document.getElementById(id); - if(element.style.display === 'block') { - element.style.display = 'none'; - } else { - element.style.display = 'block'; - } + function blanket_toggleSource(id) { + var element = document.getElementById(id); + if (element.style.display === 'block') { + element.style.display = 'none'; + } else { + element.style.display = 'block'; } + } - - /*var script = document.createElement("script"); + /*var script = document.createElement("script"); script.type = "text/javascript"; script.text = blanket_toggleSource.toString().replace('function ' + blanket_toggleSource.name, 'function blanket_toggleSource'); body.appendChild(script);*/ - (1,eval)(blanket_toggleSource.toString().replace('function ' + blanket_toggleSource.name, 'function blanket_toggleSource')); + (1, eval)( + blanket_toggleSource + .toString() + .replace( + 'function ' + blanket_toggleSource.name, + 'function blanket_toggleSource' + ) + ); - var percentage = function(number, total) { - return (Math.round(((number/total) * 100)*100)/100); - }; + var percentage = function(number, total) { + return Math.round(number / total * 100 * 100) / 100; + }; - var appendTag = function (type, el, str) { - var dom = document.createElement(type); - dom.innerHTML = str; - el.appendChild(dom); - }; + var appendTag = function(type, el, str) { + var dom = document.createElement(type); + dom.innerHTML = str; + el.appendChild(dom); + }; - function escapeInvalidXmlChars(str) { - return str.replace(/\&/g, "&") - .replace(//g, ">") - .replace(/\"/g, """) - .replace(/\'/g, "'"); + function escapeInvalidXmlChars(str) { + return str + .replace(/\&/g, '&') + .replace(//g, '>') + .replace(/\"/g, '"') + .replace(/\'/g, '''); + } + + function isBranchFollowed(data, bool) { + var mode = bool ? 0 : 1; + if ( + typeof data === 'undefined' || + typeof data === null || + typeof data[mode] === 'undefined' + ) { + return false; } + return data[mode].length > 0; + } - function isBranchFollowed(data,bool){ - var mode = bool ? 0 : 1; - if (typeof data === 'undefined' || - typeof data === null || - typeof data[mode] === 'undefined'){ - return false; - } - return data[mode].length > 0; - } + var branchStack = []; - var branchStack = []; - - function branchReport(colsIndex,src,cols,offset,lineNum){ - var newsrc=""; - var postfix=""; - if (branchStack.length > 0){ - newsrc += ""; - if (branchStack[0][0].end.line === lineNum){ - newsrc += escapeInvalidXmlChars(src.slice(0,branchStack[0][0].end.column)) + ""; - src = src.slice(branchStack[0][0].end.column); - branchStack.shift(); - if (branchStack.length > 0){ - newsrc += ""; - if (branchStack[0][0].end.line === lineNum){ - newsrc += escapeInvalidXmlChars(src.slice(0,branchStack[0][0].end.column)) + ""; - src = src.slice(branchStack[0][0].end.column); - branchStack.shift(); - if (!cols){ - return {src: newsrc + escapeInvalidXmlChars(src) ,cols:cols}; - } + function branchReport(colsIndex, src, cols, offset, lineNum) { + var newsrc = ''; + var postfix = ''; + if (branchStack.length > 0) { + newsrc += + ""; + if (branchStack[0][0].end.line === lineNum) { + newsrc += + escapeInvalidXmlChars(src.slice(0, branchStack[0][0].end.column)) + + ''; + src = src.slice(branchStack[0][0].end.column); + branchStack.shift(); + if (branchStack.length > 0) { + newsrc += + ""; + if (branchStack[0][0].end.line === lineNum) { + newsrc += + escapeInvalidXmlChars( + src.slice(0, branchStack[0][0].end.column) + ) + ''; + src = src.slice(branchStack[0][0].end.column); + branchStack.shift(); + if (!cols) { + return { src: newsrc + escapeInvalidXmlChars(src), cols: cols }; } - else if (!cols){ - return {src: newsrc + escapeInvalidXmlChars(src) + "",cols:cols}; - } - else{ - postfix = ""; - } - }else if (!cols){ - return {src: newsrc + escapeInvalidXmlChars(src) ,cols:cols}; + } else if (!cols) { + return { + src: newsrc + escapeInvalidXmlChars(src) + '', + cols: cols, + }; + } else { + postfix = ''; } - }else if(!cols){ - return {src: newsrc + escapeInvalidXmlChars(src) + "",cols:cols}; - }else{ - postfix = ""; + } else if (!cols) { + return { src: newsrc + escapeInvalidXmlChars(src), cols: cols }; } + } else if (!cols) { + return { + src: newsrc + escapeInvalidXmlChars(src) + '', + cols: cols, + }; + } else { + postfix = ''; } - var thisline = cols[colsIndex]; - //consequent - - var cons = thisline.consequent; - if (cons.start.line > lineNum){ - branchStack.unshift([thisline.alternate,thisline]); - branchStack.unshift([cons,thisline]); - src = escapeInvalidXmlChars(src); - }else{ - var style = ""; - newsrc += escapeInvalidXmlChars(src.slice(0,cons.start.column-offset)) + style; - - if (cols.length > colsIndex+1 && - cols[colsIndex+1].consequent.start.line === lineNum && - cols[colsIndex+1].consequent.start.column-offset < cols[colsIndex].consequent.end.column-offset) - { - var res = branchReport(colsIndex+1,src.slice(cons.start.column-offset,cons.end.column-offset),cols,cons.start.column-offset,lineNum); - newsrc += res.src; - cols = res.cols; - cols[colsIndex+1] = cols[colsIndex+2]; + } + var thisline = cols[colsIndex]; + //consequent + + var cons = thisline.consequent; + if (cons.start.line > lineNum) { + branchStack.unshift([thisline.alternate, thisline]); + branchStack.unshift([cons, thisline]); + src = escapeInvalidXmlChars(src); + } else { + var style = + ""; + newsrc += + escapeInvalidXmlChars(src.slice(0, cons.start.column - offset)) + style; + + if ( + cols.length > colsIndex + 1 && + cols[colsIndex + 1].consequent.start.line === lineNum && + cols[colsIndex + 1].consequent.start.column - offset < + cols[colsIndex].consequent.end.column - offset + ) { + var res = branchReport( + colsIndex + 1, + src.slice(cons.start.column - offset, cons.end.column - offset), + cols, + cons.start.column - offset, + lineNum + ); + newsrc += res.src; + cols = res.cols; + cols[colsIndex + 1] = cols[colsIndex + 2]; + cols.length--; + } else { + newsrc += escapeInvalidXmlChars( + src.slice(cons.start.column - offset, cons.end.column - offset) + ); + } + newsrc += ''; + + var alt = thisline.alternate; + if (alt.start.line > lineNum) { + newsrc += escapeInvalidXmlChars(src.slice(cons.end.column - offset)); + branchStack.unshift([alt, thisline]); + } else { + newsrc += escapeInvalidXmlChars( + src.slice(cons.end.column - offset, alt.start.column - offset) + ); + style = + ""; + newsrc += style; + if ( + cols.length > colsIndex + 1 && + cols[colsIndex + 1].consequent.start.line === lineNum && + cols[colsIndex + 1].consequent.start.column - offset < + cols[colsIndex].alternate.end.column - offset + ) { + var res2 = branchReport( + colsIndex + 1, + src.slice(alt.start.column - offset, alt.end.column - offset), + cols, + alt.start.column - offset, + lineNum + ); + newsrc += res2.src; + cols = res2.cols; + cols[colsIndex + 1] = cols[colsIndex + 2]; cols.length--; - }else{ - newsrc += escapeInvalidXmlChars(src.slice(cons.start.column-offset,cons.end.column-offset)); + } else { + newsrc += escapeInvalidXmlChars( + src.slice(alt.start.column - offset, alt.end.column - offset) + ); } - newsrc += ""; + newsrc += ''; + newsrc += escapeInvalidXmlChars(src.slice(alt.end.column - offset)); + src = newsrc; + } + } + return { src: src + postfix, cols: cols }; + } - var alt = thisline.alternate; - if (alt.start.line > lineNum){ - newsrc += escapeInvalidXmlChars(src.slice(cons.end.column-offset)); - branchStack.unshift([alt,thisline]); - }else{ - newsrc += escapeInvalidXmlChars(src.slice(cons.end.column-offset,alt.start.column-offset)); - style = ""; - newsrc += style; - if (cols.length > colsIndex+1 && - cols[colsIndex+1].consequent.start.line === lineNum && - cols[colsIndex+1].consequent.start.column-offset < cols[colsIndex].alternate.end.column-offset) - { - var res2 = branchReport(colsIndex+1,src.slice(alt.start.column-offset,alt.end.column-offset),cols,alt.start.column-offset,lineNum); - newsrc += res2.src; - cols = res2.cols; - cols[colsIndex+1] = cols[colsIndex+2]; - cols.length--; - }else{ - newsrc += escapeInvalidXmlChars(src.slice(alt.start.column-offset,alt.end.column-offset)); - } - newsrc += ""; - newsrc += escapeInvalidXmlChars(src.slice(alt.end.column-offset)); - src = newsrc; + var isUndefined = function(item) { + return typeof item !== 'undefined'; + }; + + var files = coverage.files; + var totals = { + totalSmts: 0, + numberOfFilesCovered: 0, + passedBranches: 0, + totalBranches: 0, + moduleTotalStatements: {}, + moduleTotalCoveredStatements: {}, + moduleTotalBranches: {}, + moduleTotalCoveredBranches: {}, + }; + + // check if a data-cover-modulepattern was provided for per-module coverage reporting + var modulePattern = _blanket.options('modulePattern'); + var modulePatternRegex = modulePattern ? new RegExp(modulePattern) : null; + + for (var file in files) { + if (!files.hasOwnProperty(file)) { + continue; + } + + fileNumber++; + + var statsForFile = files[file], + totalSmts = 0, + numberOfFilesCovered = 0, + code = [], + i; + + var end = []; + for (i = 0; i < statsForFile.source.length; i += 1) { + var src = statsForFile.source[i]; + + if ( + branchStack.length > 0 || + typeof statsForFile.branchData !== 'undefined' + ) { + if (typeof statsForFile.branchData[i + 1] !== 'undefined') { + var cols = statsForFile.branchData[i + 1].filter(isUndefined); + var colsIndex = 0; + + src = branchReport(colsIndex, src, cols, 0, i + 1).src; + } else if (branchStack.length) { + src = branchReport(0, src, null, 0, i + 1).src; + } else { + src = escapeInvalidXmlChars(src); + } + } else { + src = escapeInvalidXmlChars(src); + } + var lineClass = ''; + if (statsForFile[i + 1]) { + numberOfFilesCovered += 1; + totalSmts += 1; + lineClass = 'hit'; + } else { + if (statsForFile[i + 1] === 0) { + totalSmts++; + lineClass = 'miss'; } } - return {src:src+postfix, cols:cols}; + code[i + 1] = + "
" + + (i + 1) + + '' + + src + + '
'; } - - var isUndefined = function(item){ - return typeof item !== 'undefined'; - }; - - var files = coverage.files; - var totals = { - totalSmts: 0, - numberOfFilesCovered: 0, - passedBranches: 0, - totalBranches: 0, - moduleTotalStatements : {}, - moduleTotalCoveredStatements : {}, - moduleTotalBranches : {}, - moduleTotalCoveredBranches : {} - }; - - // check if a data-cover-modulepattern was provided for per-module coverage reporting - var modulePattern = _blanket.options("modulePattern"); - var modulePatternRegex = ( modulePattern ? new RegExp(modulePattern) : null ); - - for(var file in files) - { - if (!files.hasOwnProperty(file)) { - continue; - } - - fileNumber++; - - var statsForFile = files[file], - totalSmts = 0, - numberOfFilesCovered = 0, - code = [], - i; - - - var end = []; - for(i = 0; i < statsForFile.source.length; i +=1){ - var src = statsForFile.source[i]; - - if (branchStack.length > 0 || - typeof statsForFile.branchData !== 'undefined') - { - if (typeof statsForFile.branchData[i+1] !== 'undefined') - { - var cols = statsForFile.branchData[i+1].filter(isUndefined); - var colsIndex=0; - - - src = branchReport(colsIndex,src,cols,0,i+1).src; - - }else if (branchStack.length){ - src = branchReport(0,src,null,0,i+1).src; - }else{ - src = escapeInvalidXmlChars(src); - } - }else{ - src = escapeInvalidXmlChars(src); - } - var lineClass=""; - if(statsForFile[i+1]) { - numberOfFilesCovered += 1; - totalSmts += 1; - lineClass = 'hit'; - }else{ - if(statsForFile[i+1] === 0){ - totalSmts++; - lineClass = 'miss'; - } - } - code[i + 1] = "
"+(i + 1)+""+src+"
"; - } - totals.totalSmts += totalSmts; - totals.numberOfFilesCovered += numberOfFilesCovered; - var totalBranches=0; - var passedBranches=0; - if (typeof statsForFile.branchData !== 'undefined'){ - for(var j=0;j 0 && - typeof statsForFile.branchData[j][k][1] !== 'undefined' && - statsForFile.branchData[j][k][1].length > 0){ - passedBranches++; - } - } + totals.totalSmts += totalSmts; + totals.numberOfFilesCovered += numberOfFilesCovered; + var totalBranches = 0; + var passedBranches = 0; + if (typeof statsForFile.branchData !== 'undefined') { + for (var j = 0; j < statsForFile.branchData.length; j++) { + if (typeof statsForFile.branchData[j] !== 'undefined') { + for (var k = 0; k < statsForFile.branchData[j].length; k++) { + if (typeof statsForFile.branchData[j][k] !== 'undefined') { + totalBranches++; + if ( + typeof statsForFile.branchData[j][k][0] !== 'undefined' && + statsForFile.branchData[j][k][0].length > 0 && + typeof statsForFile.branchData[j][k][1] !== 'undefined' && + statsForFile.branchData[j][k][1].length > 0 + ) { + passedBranches++; } } } } - totals.passedBranches += passedBranches; - totals.totalBranches += totalBranches; - - // if "data-cover-modulepattern" was provided, - // track totals per module name as well as globally - if (modulePatternRegex) { - var moduleName = file.match(modulePatternRegex)[1]; - - if(!totals.moduleTotalStatements.hasOwnProperty(moduleName)) { - totals.moduleTotalStatements[moduleName] = 0; - totals.moduleTotalCoveredStatements[moduleName] = 0; - } - - totals.moduleTotalStatements[moduleName] += totalSmts; - totals.moduleTotalCoveredStatements[moduleName] += numberOfFilesCovered; - - if(!totals.moduleTotalBranches.hasOwnProperty(moduleName)) { - totals.moduleTotalBranches[moduleName] = 0; - totals.moduleTotalCoveredBranches[moduleName] = 0; - } - - totals.moduleTotalBranches[moduleName] += totalBranches; - totals.moduleTotalCoveredBranches[moduleName] += passedBranches; - } - - var result = percentage(numberOfFilesCovered, totalSmts); - - var output = fileTemplate.replace("{{file}}", file) - .replace("{{percentage}}",result) - .replace("{{numberCovered}}", numberOfFilesCovered) - .replace(/\{\{fileNumber\}\}/g, fileNumber) - .replace("{{totalSmts}}", totalSmts) - .replace("{{totalBranches}}", totalBranches) - .replace("{{passedBranches}}", passedBranches) - .replace("{{source}}", code.join(" ")); - if(result < successRate) - { - output = output.replace("{{statusclass}}", "bl-error"); - } else { - output = output.replace("{{statusclass}}", "bl-success"); - } - bodyContent += output; + } } + totals.passedBranches += passedBranches; + totals.totalBranches += totalBranches; - // create temporary function for use by the global totals reporter, - // as well as the per-module totals reporter - var createAggregateTotal = function(numSt, numCov, numBranch, numCovBr, moduleName) { - - var totalPercent = percentage(numCov, numSt); - var statusClass = totalPercent < successRate ? "bl-error" : "bl-success"; - var rowTitle = ( moduleName ? "Total for module: " + moduleName : "Global total" ); - var totalsOutput = grandTotalTemplate.replace("{{rowTitle}}", rowTitle) - .replace("{{percentage}}", totalPercent) - .replace("{{numberCovered}}", numCov) - .replace("{{totalSmts}}", numSt) - .replace("{{passedBranches}}", numCovBr) - .replace("{{totalBranches}}", numBranch) - .replace("{{statusclass}}", statusClass); - - bodyContent += totalsOutput; - }; - - // if "data-cover-modulepattern" was provided, - // output the per-module totals alongside the global totals + // if "data-cover-modulepattern" was provided, + // track totals per module name as well as globally if (modulePatternRegex) { - for (var thisModuleName in totals.moduleTotalStatements) { - if (totals.moduleTotalStatements.hasOwnProperty(thisModuleName)) { + var moduleName = file.match(modulePatternRegex)[1]; - var moduleTotalSt = totals.moduleTotalStatements[thisModuleName]; - var moduleTotalCovSt = totals.moduleTotalCoveredStatements[thisModuleName]; + if (!totals.moduleTotalStatements.hasOwnProperty(moduleName)) { + totals.moduleTotalStatements[moduleName] = 0; + totals.moduleTotalCoveredStatements[moduleName] = 0; + } - var moduleTotalBr = totals.moduleTotalBranches[thisModuleName]; - var moduleTotalCovBr = totals.moduleTotalCoveredBranches[thisModuleName]; + totals.moduleTotalStatements[moduleName] += totalSmts; + totals.moduleTotalCoveredStatements[moduleName] += numberOfFilesCovered; - createAggregateTotal(moduleTotalSt, moduleTotalCovSt, moduleTotalBr, moduleTotalCovBr, thisModuleName); - } - } + if (!totals.moduleTotalBranches.hasOwnProperty(moduleName)) { + totals.moduleTotalBranches[moduleName] = 0; + totals.moduleTotalCoveredBranches[moduleName] = 0; + } + + totals.moduleTotalBranches[moduleName] += totalBranches; + totals.moduleTotalCoveredBranches[moduleName] += passedBranches; } - createAggregateTotal(totals.totalSmts, totals.numberOfFilesCovered, totals.totalBranches, totals.passedBranches, null); - bodyContent += "
"; //closing main + var result = percentage(numberOfFilesCovered, totalSmts); + var output = fileTemplate + .replace('{{file}}', file) + .replace('{{percentage}}', result) + .replace('{{numberCovered}}', numberOfFilesCovered) + .replace(/\{\{fileNumber\}\}/g, fileNumber) + .replace('{{totalSmts}}', totalSmts) + .replace('{{totalBranches}}', totalBranches) + .replace('{{passedBranches}}', passedBranches) + .replace('{{source}}', code.join(' ')); + if (result < successRate) { + output = output.replace('{{statusclass}}', 'bl-error'); + } else { + output = output.replace('{{statusclass}}', 'bl-success'); + } + bodyContent += output; + } - appendTag('style', head, cssSytle); - //appendStyle(body, headerContent); - if (document.getElementById("blanket-main")){ - document.getElementById("blanket-main").innerHTML= - bodyContent.slice(23,-6); - }else{ - appendTag('div', body, bodyContent); + // create temporary function for use by the global totals reporter, + // as well as the per-module totals reporter + var createAggregateTotal = function( + numSt, + numCov, + numBranch, + numCovBr, + moduleName + ) { + var totalPercent = percentage(numCov, numSt); + var statusClass = totalPercent < successRate ? 'bl-error' : 'bl-success'; + var rowTitle = moduleName + ? 'Total for module: ' + moduleName + : 'Global total'; + var totalsOutput = grandTotalTemplate + .replace('{{rowTitle}}', rowTitle) + .replace('{{percentage}}', totalPercent) + .replace('{{numberCovered}}', numCov) + .replace('{{totalSmts}}', numSt) + .replace('{{passedBranches}}', numCovBr) + .replace('{{totalBranches}}', numBranch) + .replace('{{statusclass}}', statusClass); + + bodyContent += totalsOutput; + }; + + // if "data-cover-modulepattern" was provided, + // output the per-module totals alongside the global totals + if (modulePatternRegex) { + for (var thisModuleName in totals.moduleTotalStatements) { + if (totals.moduleTotalStatements.hasOwnProperty(thisModuleName)) { + var moduleTotalSt = totals.moduleTotalStatements[thisModuleName]; + var moduleTotalCovSt = + totals.moduleTotalCoveredStatements[thisModuleName]; + + var moduleTotalBr = totals.moduleTotalBranches[thisModuleName]; + var moduleTotalCovBr = + totals.moduleTotalCoveredBranches[thisModuleName]; + + createAggregateTotal( + moduleTotalSt, + moduleTotalCovSt, + moduleTotalBr, + moduleTotalCovBr, + thisModuleName + ); + } + } + } + + createAggregateTotal( + totals.totalSmts, + totals.numberOfFilesCovered, + totals.totalBranches, + totals.passedBranches, + null + ); + bodyContent += '
'; //closing main + + appendTag('style', head, cssSytle); + //appendStyle(body, headerContent); + if (document.getElementById('blanket-main')) { + document.getElementById('blanket-main').innerHTML = bodyContent.slice( + 23, + -6 + ); + } else { + appendTag('div', body, bodyContent); + } + + fileNumber = 0; + for (var file in files) { + if (!files.hasOwnProperty(file)) { + continue; } - fileNumber = 0; - for(var file in files) { - if (!files.hasOwnProperty(file)) { - continue; - } + fileNumber++; + var _ = (function() { + var localFN = fileNumber; + $('#blanket-link-file-' + fileNumber).click(function() { + blanket_toggleSource('file-' + localFN); + }); + })(); + } - fileNumber++; - var _ = function() { - var localFN = fileNumber; - $("#blanket-link-file-" + fileNumber).click(function() { - blanket_toggleSource("file-" + localFN); - }); - }(); - } - - //appendHtml(body, ''); + //appendHtml(body, ''); }; -(function(){ - var newOptions={}; - //http://stackoverflow.com/a/2954896 - var toArray =Array.prototype.slice; - var scripts = toArray.call(document.scripts); - toArray.call(scripts[scripts.length - 1].attributes) - .forEach(function(es){ - if(es.nodeName === "data-cover-only"){ - newOptions.filter = es.nodeValue; - } - if(es.nodeName === "data-cover-never"){ - newOptions.antifilter = es.nodeValue; - } - if(es.nodeName === "data-cover-reporter"){ - newOptions.reporter = es.nodeValue; - } - if (es.nodeName === "data-cover-adapter"){ - newOptions.adapter = es.nodeValue; - } - if (es.nodeName === "data-cover-loader"){ - newOptions.loader = es.nodeValue; - } - if (es.nodeName === "data-cover-timeout"){ - newOptions.timeout = es.nodeValue; - } - if (es.nodeName === "data-cover-modulepattern") { - newOptions.modulePattern = es.nodeValue; - } - if (es.nodeName === "data-cover-reporter-options"){ - try{ - newOptions.reporter_options = JSON.parse(es.nodeValue); - }catch(e){ - if (blanket.options("debug")){ - throw new Error("Invalid reporter options. Must be a valid stringified JSON object."); - } - } - } - if (es.nodeName === "data-cover-testReadyCallback"){ - newOptions.testReadyCallback = es.nodeValue; - } - if (es.nodeName === "data-cover-customVariable"){ - newOptions.customVariable = es.nodeValue; - } - if (es.nodeName === "data-cover-flags"){ - var flags = " "+es.nodeValue+" "; - if (flags.indexOf(" ignoreError ") > -1){ - newOptions.ignoreScriptError = true; - } - if (flags.indexOf(" autoStart ") > -1){ - newOptions.autoStart = true; - } - if (flags.indexOf(" ignoreCors ") > -1){ - newOptions.ignoreCors = true; - } - if (flags.indexOf(" branchTracking ") > -1){ - newOptions.branchTracking = true; - } - if (flags.indexOf(" sourceURL ") > -1){ - newOptions.sourceURL = true; - } - if (flags.indexOf(" debug ") > -1){ - newOptions.debug = true; - } - if (flags.indexOf(" engineOnly ") > -1){ - newOptions.engineOnly = true; - } - if (flags.indexOf(" commonJS ") > -1){ - newOptions.commonJS = true; - } - if (flags.indexOf(" instrumentCache ") > -1){ - newOptions.instrumentCache = true; - } - } - }); - blanket.options(newOptions); +(function() { + var newOptions = {}; + //http://stackoverflow.com/a/2954896 + var toArray = Array.prototype.slice; + var scripts = toArray.call(document.scripts); + toArray.call(scripts[scripts.length - 1].attributes).forEach(function(es) { + if (es.nodeName === 'data-cover-only') { + newOptions.filter = es.nodeValue; + } + if (es.nodeName === 'data-cover-never') { + newOptions.antifilter = es.nodeValue; + } + if (es.nodeName === 'data-cover-reporter') { + newOptions.reporter = es.nodeValue; + } + if (es.nodeName === 'data-cover-adapter') { + newOptions.adapter = es.nodeValue; + } + if (es.nodeName === 'data-cover-loader') { + newOptions.loader = es.nodeValue; + } + if (es.nodeName === 'data-cover-timeout') { + newOptions.timeout = es.nodeValue; + } + if (es.nodeName === 'data-cover-modulepattern') { + newOptions.modulePattern = es.nodeValue; + } + if (es.nodeName === 'data-cover-reporter-options') { + try { + newOptions.reporter_options = JSON.parse(es.nodeValue); + } catch (e) { + if (blanket.options('debug')) { + throw new Error( + 'Invalid reporter options. Must be a valid stringified JSON object.' + ); + } + } + } + if (es.nodeName === 'data-cover-testReadyCallback') { + newOptions.testReadyCallback = es.nodeValue; + } + if (es.nodeName === 'data-cover-customVariable') { + newOptions.customVariable = es.nodeValue; + } + if (es.nodeName === 'data-cover-flags') { + var flags = ' ' + es.nodeValue + ' '; + if (flags.indexOf(' ignoreError ') > -1) { + newOptions.ignoreScriptError = true; + } + if (flags.indexOf(' autoStart ') > -1) { + newOptions.autoStart = true; + } + if (flags.indexOf(' ignoreCors ') > -1) { + newOptions.ignoreCors = true; + } + if (flags.indexOf(' branchTracking ') > -1) { + newOptions.branchTracking = true; + } + if (flags.indexOf(' sourceURL ') > -1) { + newOptions.sourceURL = true; + } + if (flags.indexOf(' debug ') > -1) { + newOptions.debug = true; + } + if (flags.indexOf(' engineOnly ') > -1) { + newOptions.engineOnly = true; + } + if (flags.indexOf(' commonJS ') > -1) { + newOptions.commonJS = true; + } + if (flags.indexOf(' instrumentCache ') > -1) { + newOptions.instrumentCache = true; + } + } + }); + blanket.options(newOptions); - if (typeof requirejs !== 'undefined'){ - blanket.options("existingRequireJS",true); - } - /* setup requirejs loader, if needed */ - - if (blanket.options("commonJS")){ - blanket._commonjs = {}; - } + if (typeof requirejs !== 'undefined') { + blanket.options('existingRequireJS', true); + } + /* setup requirejs loader, if needed */ + + if (blanket.options('commonJS')) { + blanket._commonjs = {}; + } })(); -(function(_blanket){ -_blanket.extend({ +(function(_blanket) { + _blanket.extend({ utils: { - normalizeBackslashes: function(str) { - return str.replace(/\\/g, '/'); - }, - matchPatternAttribute: function(filename,pattern){ - if (typeof pattern === 'string'){ - if (pattern.indexOf("[") === 0){ - //treat as array - var pattenArr = pattern.slice(1,pattern.length-1).split(","); - return pattenArr.some(function(elem){ - return _blanket.utils.matchPatternAttribute(filename,_blanket.utils.normalizeBackslashes(elem.slice(1,-1))); - //return filename.indexOf(_blanket.utils.normalizeBackslashes(elem.slice(1,-1))) > -1; - }); - }else if ( pattern.indexOf("//") === 0){ - var ex = pattern.slice(2,pattern.lastIndexOf('/')); - var mods = pattern.slice(pattern.lastIndexOf('/')+1); - var regex = new RegExp(ex,mods); - return regex.test(filename); - }else if (pattern.indexOf("#") === 0){ - return window[pattern.slice(1)].call(window,filename); - }else{ - return filename.indexOf(_blanket.utils.normalizeBackslashes(pattern)) > -1; - } - }else if ( pattern instanceof Array ){ - return pattern.some(function(elem){ - return _blanket.utils.matchPatternAttribute(filename,elem); - }); - }else if (pattern instanceof RegExp){ - return pattern.test(filename); - }else if (typeof pattern === "function"){ - return pattern.call(window,filename); - } - }, - blanketEval: function(data){ - _blanket._addScript(data); - }, - collectPageScripts: function(){ - var toArray = Array.prototype.slice; - var scripts = toArray.call(document.scripts); - var selectedScripts=[],scriptNames=[]; - var filter = _blanket.options("filter"); - if(filter != null){ - //global filter in place, data-cover-only - var antimatch = _blanket.options("antifilter"); - selectedScripts = toArray.call(document.scripts) - .filter(function(s){ - return toArray.call(s.attributes).filter(function(sn){ - return sn.nodeName === "src" && _blanket.utils.matchPatternAttribute(sn.nodeValue,filter) && - (typeof antimatch === "undefined" || !_blanket.utils.matchPatternAttribute(sn.nodeValue,antimatch)); - }).length === 1; - }); - }else{ - selectedScripts = toArray.call(document.querySelectorAll("script[data-cover]")); - } - scriptNames = selectedScripts.map(function(s){ - return _blanket.utils.qualifyURL( - toArray.call(s.attributes).filter( - function(sn){ - return sn.nodeName === "src"; - })[0].nodeValue); - }); - if (!filter){ - _blanket.options("filter","['"+scriptNames.join("','")+"']"); - } - return scriptNames; - }, - loadAll: function(nextScript,cb,preprocessor){ - /** - * load dependencies - * @param {nextScript} factory for priority level - * @param {cb} the done callback - */ - var currScript=nextScript(); - var isLoaded = _blanket.utils.scriptIsLoaded( - currScript, - _blanket.utils.ifOrdered, - nextScript, - cb - ); - - if (!(_blanket.utils.cache[currScript] && _blanket.utils.cache[currScript].loaded)){ - var attach = function(){ - if (_blanket.options("debug")) {console.log("BLANKET-Mark script:"+currScript+", as loaded and move to next script.");} - isLoaded(); - }; - var whenDone = function(result){ - if (_blanket.options("debug")) {console.log("BLANKET-File loading finished");} - if (typeof result !== 'undefined'){ - if (_blanket.options("debug")) {console.log("BLANKET-Add file to DOM.");} - _blanket._addScript(result); - } - attach(); - }; - - _blanket.utils.attachScript( - { - url: currScript - }, - function (content){ - _blanket.utils.processFile( - content, - currScript, - whenDone, - whenDone - ); - } - ); - }else{ - isLoaded(); - } - }, - attachScript: function(options,cb){ - var timeout = _blanket.options("timeout") || 3000; - setTimeout(function(){ - if (!_blanket.utils.cache[options.url].loaded){ - throw new Error("error loading source script"); - } - },timeout); - _blanket.utils.getFile( - options.url, - cb, - function(){ throw new Error("error loading source script");} + normalizeBackslashes: function(str) { + return str.replace(/\\/g, '/'); + }, + matchPatternAttribute: function(filename, pattern) { + if (typeof pattern === 'string') { + if (pattern.indexOf('[') === 0) { + //treat as array + var pattenArr = pattern.slice(1, pattern.length - 1).split(','); + return pattenArr.some(function(elem) { + return _blanket.utils.matchPatternAttribute( + filename, + _blanket.utils.normalizeBackslashes(elem.slice(1, -1)) + ); + //return filename.indexOf(_blanket.utils.normalizeBackslashes(elem.slice(1,-1))) > -1; + }); + } else if (pattern.indexOf('//') === 0) { + var ex = pattern.slice(2, pattern.lastIndexOf('/')); + var mods = pattern.slice(pattern.lastIndexOf('/') + 1); + var regex = new RegExp(ex, mods); + return regex.test(filename); + } else if (pattern.indexOf('#') === 0) { + return window[pattern.slice(1)].call(window, filename); + } else { + return ( + filename.indexOf(_blanket.utils.normalizeBackslashes(pattern)) > + -1 ); - }, - ifOrdered: function(nextScript,cb){ - /** - * ordered loading callback - * @param {nextScript} factory for priority level - * @param {cb} the done callback - */ - var currScript = nextScript(true); - if (currScript){ - _blanket.utils.loadAll(nextScript,cb); - }else{ - cb(new Error("Error in loading chain.")); - } - }, - scriptIsLoaded: function(url,orderedCb,nextScript,cb){ - /** - * returns a callback that checks a loading list to see if a script is loaded. - * @param {orderedCb} callback if ordered loading is being done - * @param {nextScript} factory for next priority level - * @param {cb} the done callback - */ - if (_blanket.options("debug")) {console.log("BLANKET-Returning function");} - return function(){ - if (_blanket.options("debug")) {console.log("BLANKET-Marking file as loaded: "+url);} - - _blanket.utils.cache[url].loaded=true; - - if (_blanket.utils.allLoaded()){ - if (_blanket.options("debug")) {console.log("BLANKET-All files loaded");} - cb(); - }else if (orderedCb){ - //if it's ordered we need to - //traverse down to the next - //priority level - if (_blanket.options("debug")) {console.log("BLANKET-Load next file.");} - orderedCb(nextScript,cb); - } - }; - }, - cache: {}, - allLoaded: function (){ - /** - * check if depdencies are loaded in cache - */ - var cached = Object.keys(_blanket.utils.cache); - for (var i=0;i -1){ - callback(_blanket.blanketSession[key]); - foundInSession=true; - return; - } - } + isLoaded(); + }; + var whenDone = function(result) { + if (_blanket.options('debug')) { + console.log('BLANKET-File loading finished'); } - if (!foundInSession){ - var xhr = _blanket.utils.createXhr(); - xhr.open('GET', url, true); + if (typeof result !== 'undefined') { + if (_blanket.options('debug')) { + console.log('BLANKET-Add file to DOM.'); + } + _blanket._addScript(result); + } + attach(); + }; - //Allow overrides specified in config - if (onXhr) { - onXhr(xhr, url); + _blanket.utils.attachScript( + { + url: currScript, + }, + function(content) { + _blanket.utils.processFile( + content, + currScript, + whenDone, + whenDone + ); + } + ); + } else { + isLoaded(); + } + }, + attachScript: function(options, cb) { + var timeout = _blanket.options('timeout') || 3000; + setTimeout(function() { + if (!_blanket.utils.cache[options.url].loaded) { + throw new Error('error loading source script'); + } + }, timeout); + _blanket.utils.getFile(options.url, cb, function() { + throw new Error('error loading source script'); + }); + }, + ifOrdered: function(nextScript, cb) { + /** + * ordered loading callback + * @param {nextScript} factory for priority level + * @param {cb} the done callback + */ + var currScript = nextScript(true); + if (currScript) { + _blanket.utils.loadAll(nextScript, cb); + } else { + cb(new Error('Error in loading chain.')); + } + }, + scriptIsLoaded: function(url, orderedCb, nextScript, cb) { + /** + * returns a callback that checks a loading list to see if a script is loaded. + * @param {orderedCb} callback if ordered loading is being done + * @param {nextScript} factory for next priority level + * @param {cb} the done callback + */ + if (_blanket.options('debug')) { + console.log('BLANKET-Returning function'); + } + return function() { + if (_blanket.options('debug')) { + console.log('BLANKET-Marking file as loaded: ' + url); + } + + _blanket.utils.cache[url].loaded = true; + + if (_blanket.utils.allLoaded()) { + if (_blanket.options('debug')) { + console.log('BLANKET-All files loaded'); + } + cb(); + } else if (orderedCb) { + //if it's ordered we need to + //traverse down to the next + //priority level + if (_blanket.options('debug')) { + console.log('BLANKET-Load next file.'); + } + orderedCb(nextScript, cb); + } + }; + }, + cache: {}, + allLoaded: function() { + /** + * check if depdencies are loaded in cache + */ + var cached = Object.keys(_blanket.utils.cache); + for (var i = 0; i < cached.length; i++) { + if (!_blanket.utils.cache[cached[i]].loaded) { + return false; + } + } + return true; + }, + processFile: function(content, url, cb, oldCb) { + var match = _blanket.options('filter'); + //we check the never matches first + var antimatch = _blanket.options('antifilter'); + if ( + typeof antimatch !== 'undefined' && + _blanket.utils.matchPatternAttribute(url, antimatch) + ) { + oldCb(content); + if (_blanket.options('debug')) { + console.log('BLANKET-File will never be instrumented:' + url); + } + _blanket.requiringFile(url, true); + } else if (_blanket.utils.matchPatternAttribute(url, match)) { + if (_blanket.options('debug')) { + console.log('BLANKET-Attempting instrument of:' + url); + } + _blanket.instrument( + { + inputFile: content, + inputFileName: url, + }, + function(instrumented) { + try { + if (_blanket.options('debug')) { + console.log( + 'BLANKET-instrument of:' + url + ' was successfull.' + ); } + _blanket.utils.blanketEval(instrumented); + cb(); + _blanket.requiringFile(url, true); + } catch (err) { + if (_blanket.options('ignoreScriptError')) { + //we can continue like normal if + //we're ignoring script errors, + //but otherwise we don't want + //to completeLoad or the error might be + //missed. + if (_blanket.options('debug')) { + console.log( + 'BLANKET-There was an error loading the file:' + url + ); + } + cb(content); + _blanket.requiringFile(url, true); + } else { + throw new Error('Error parsing instrumented code: ' + err); + } + } + } + ); + } else { + if (_blanket.options('debug')) { + console.log( + 'BLANKET-Loading (without instrumenting) the file:' + url + ); + } + oldCb(content); + _blanket.requiringFile(url, true); + } + }, + cacheXhrConstructor: function() { + var Constructor, createXhr, i, progId; + if (typeof XMLHttpRequest !== 'undefined') { + Constructor = XMLHttpRequest; + this.createXhr = function() { + return new Constructor(); + }; + } else if (typeof ActiveXObject !== 'undefined') { + Constructor = ActiveXObject; + for (i = 0; i < 3; i += 1) { + progId = progIds[i]; + try { + new ActiveXObject(progId); + break; + } catch (e) {} + } + this.createXhr = function() { + return new Constructor(progId); + }; + } + }, + craeteXhr: function() { + throw new Error( + 'cacheXhrConstructor is supposed to overwrite this function.' + ); + }, + getFile: function(url, callback, errback, onXhr) { + var foundInSession = false; + if (_blanket.blanketSession) { + var files = Object.keys(_blanket.blanketSession); + for (var i = 0; i < files.length; i++) { + var key = files[i]; + if (url.indexOf(key) > -1) { + callback(_blanket.blanketSession[key]); + foundInSession = true; + return; + } + } + } + if (!foundInSession) { + var xhr = _blanket.utils.createXhr(); + xhr.open('GET', url, true); - xhr.onreadystatechange = function (evt) { - var status, err; - - //Do not explicitly handle errors, those should be - //visible via console output in the browser. - if (xhr.readyState === 4) { - status = xhr.status; - if ((status > 399 && status < 600) /*|| + //Allow overrides specified in config + if (onXhr) { + onXhr(xhr, url); + } + + xhr.onreadystatechange = function(evt) { + var status, err; + + //Do not explicitly handle errors, those should be + //visible via console output in the browser. + if (xhr.readyState === 4) { + status = xhr.status; + if ( + status > 399 && + status < + 600 /*|| (status === 0 && navigator.userAgent.toLowerCase().indexOf('firefox') > -1) - */ ) { - //An http 4xx or 5xx error. Signal an error. - err = new Error(url + ' HTTP status: ' + status); - err.xhr = xhr; - errback(err); - } else { - callback(xhr.responseText); - } - } - }; - try{ - xhr.send(null); - }catch(e){ - if (e.code && (e.code === 101 || e.code === 1012) && _blanket.options("ignoreCors") === false){ - //running locally and getting error from browser - _blanket.showManualLoader(); - } else { - throw e; - } - } + */ + ) { + //An http 4xx or 5xx error. Signal an error. + err = new Error(url + ' HTTP status: ' + status); + err.xhr = xhr; + errback(err); + } else { + callback(xhr.responseText); + } } + }; + try { + xhr.send(null); + } catch (e) { + if ( + e.code && + (e.code === 101 || e.code === 1012) && + _blanket.options('ignoreCors') === false + ) { + //running locally and getting error from browser + _blanket.showManualLoader(); + } else { + throw e; + } + } } - } -}); + }, + }, + }); -(function(){ - var require = blanket.options("commonJS") ? blanket._commonjs.require : window.require; - var requirejs = blanket.options("commonJS") ? blanket._commonjs.requirejs : window.requirejs; - if (!_blanket.options("engineOnly") && _blanket.options("existingRequireJS")){ + (function() { + var require = blanket.options('commonJS') + ? blanket._commonjs.require + : window.require; + var requirejs = blanket.options('commonJS') + ? blanket._commonjs.requirejs + : window.requirejs; + if ( + !_blanket.options('engineOnly') && + _blanket.options('existingRequireJS') + ) { + _blanket.utils.oldloader = requirejs.load; - _blanket.utils.oldloader = requirejs.load; - - requirejs.load = function (context, moduleName, url) { - _blanket.requiringFile(url); - _blanket.utils.getFile(url, - function(content){ - _blanket.utils.processFile( - content, - url, - function newLoader(){ - context.completeLoad(moduleName); - }, - function oldLoader(){ - _blanket.utils.oldloader(context, moduleName, url); - } - ); - }, function (err) { - _blanket.requiringFile(); - throw err; - }); - }; + requirejs.load = function(context, moduleName, url) { + _blanket.requiringFile(url); + _blanket.utils.getFile( + url, + function(content) { + _blanket.utils.processFile( + content, + url, + function newLoader() { + context.completeLoad(moduleName); + }, + function oldLoader() { + _blanket.utils.oldloader(context, moduleName, url); + } + ); + }, + function(err) { + _blanket.requiringFile(); + throw err; + } + ); + }; } // Save the XHR constructor, just in case frameworks like Sinon would sandbox it. _blanket.utils.cacheXhrConstructor(); -})(); - + })(); })(blanket); -(function() { - - if(!mocha) { - throw new Exception("mocha library does not exist in global namespace!"); - } - - - /* +(function() { + if (!mocha) { + throw new Exception('mocha library does not exist in global namespace!'); + } + + /* * Mocha Events: * * - `start` execution started @@ -5258,52 +6004,52 @@ _blanket.extend({ * - `pass` (test) test passed * - `fail` (test, err) test failed * - */ - - var OriginalReporter = mocha._reporter; - - var BlanketReporter = function(runner) { - runner.on('start', function() { - blanket.setupCoverage(); - }); - - runner.on('end', function() { - blanket.onTestsDone(); - }); - - runner.on('suite', function() { - blanket.onModuleStart(); - }); - - runner.on('test', function() { - blanket.onTestStart(); - }); - - runner.on('test end', function(test) { - blanket.onTestDone(test.parent.tests.length, test.state === 'passed'); - }); - - // NOTE: this is an instance of BlanketReporter - new OriginalReporter(runner); - }; - - BlanketReporter.prototype = OriginalReporter.prototype; - - mocha.reporter(BlanketReporter); - - var oldRun = mocha.run, - oldCallback = null; - - mocha.run = function (finishCallback) { - oldCallback = finishCallback; - console.log("waiting for blanket..."); - }; - blanket.beforeStartTestRunner({ - callback: function(){ - if (!blanket.options("existingRequireJS")){ - oldRun(oldCallback); - } - mocha.run = oldRun; - } - }); -})(); + */ + + var OriginalReporter = mocha._reporter; + + var BlanketReporter = function(runner) { + runner.on('start', function() { + blanket.setupCoverage(); + }); + + runner.on('end', function() { + blanket.onTestsDone(); + }); + + runner.on('suite', function() { + blanket.onModuleStart(); + }); + + runner.on('test', function() { + blanket.onTestStart(); + }); + + runner.on('test end', function(test) { + blanket.onTestDone(test.parent.tests.length, test.state === 'passed'); + }); + + // NOTE: this is an instance of BlanketReporter + new OriginalReporter(runner); + }; + + BlanketReporter.prototype = OriginalReporter.prototype; + + mocha.reporter(BlanketReporter); + + var oldRun = mocha.run, + oldCallback = null; + + mocha.run = function(finishCallback) { + oldCallback = finishCallback; + console.log('waiting for blanket...'); + }; + blanket.beforeStartTestRunner({ + callback: function() { + if (!blanket.options('existingRequireJS')) { + oldRun(oldCallback); + } + mocha.run = oldRun; + }, + }); +})(); diff --git a/test/conversation_controller_test.js b/test/conversation_controller_test.js index b651ac6d2..2bd5cea9e 100644 --- a/test/conversation_controller_test.js +++ b/test/conversation_controller_test.js @@ -5,25 +5,35 @@ describe('ConversationController', function() { var collection = window.getInboxCollection(); collection.reset([]); - collection.add(new Whisper.Conversation({ - name: 'No timestamp', - })); - collection.add(new Whisper.Conversation({ - name: 'B', - timestamp: 20, - })); - collection.add(new Whisper.Conversation({ - name: 'C', - timestamp: 20, - })); - collection.add(new Whisper.Conversation({ - name: 'Á', - timestamp: 20, - })); - collection.add(new Whisper.Conversation({ - name: 'First!', - timestamp: 30, - })); + collection.add( + new Whisper.Conversation({ + name: 'No timestamp', + }) + ); + collection.add( + new Whisper.Conversation({ + name: 'B', + timestamp: 20, + }) + ); + collection.add( + new Whisper.Conversation({ + name: 'C', + timestamp: 20, + }) + ); + collection.add( + new Whisper.Conversation({ + name: 'Á', + timestamp: 20, + }) + ); + collection.add( + new Whisper.Conversation({ + name: 'First!', + timestamp: 30, + }) + ); assert.strictEqual(collection.at('0').get('name'), 'First!'); assert.strictEqual(collection.at('1').get('name'), 'Á'); diff --git a/test/crypto_test.js b/test/crypto_test.js index 2f728206d..fb7894207 100644 --- a/test/crypto_test.js +++ b/test/crypto_test.js @@ -3,7 +3,10 @@ describe('Crypto', function() { it('roundtrip symmetric encryption succeeds', async function() { var message = 'this is my message'; - var plaintext = new dcodeIO.ByteBuffer.wrap(message, 'binary').toArrayBuffer(); + var plaintext = new dcodeIO.ByteBuffer.wrap( + message, + 'binary' + ).toArrayBuffer(); var key = textsecure.crypto.getRandomBytes(32); var encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext); @@ -17,7 +20,10 @@ describe('Crypto', function() { it('roundtrip fails if nonce is modified', async function() { var message = 'this is my message'; - var plaintext = new dcodeIO.ByteBuffer.wrap(message, 'binary').toArrayBuffer(); + var plaintext = new dcodeIO.ByteBuffer.wrap( + message, + 'binary' + ).toArrayBuffer(); var key = textsecure.crypto.getRandomBytes(32); var encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext); @@ -25,7 +31,10 @@ describe('Crypto', function() { uintArray[2] = 9; try { - var decrypted = await Signal.Crypto.decryptSymmetric(key, uintArray.buffer); + var decrypted = await Signal.Crypto.decryptSymmetric( + key, + uintArray.buffer + ); } catch (error) { assert.strictEqual( error.message, @@ -39,7 +48,10 @@ describe('Crypto', function() { it('fails if mac is modified', async function() { var message = 'this is my message'; - var plaintext = new dcodeIO.ByteBuffer.wrap(message, 'binary').toArrayBuffer(); + var plaintext = new dcodeIO.ByteBuffer.wrap( + message, + 'binary' + ).toArrayBuffer(); var key = textsecure.crypto.getRandomBytes(32); var encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext); @@ -47,7 +59,10 @@ describe('Crypto', function() { uintArray[uintArray.length - 3] = 9; try { - var decrypted = await Signal.Crypto.decryptSymmetric(key, uintArray.buffer); + var decrypted = await Signal.Crypto.decryptSymmetric( + key, + uintArray.buffer + ); } catch (error) { assert.strictEqual( error.message, @@ -61,7 +76,10 @@ describe('Crypto', function() { it('fails if encrypted contents are modified', async function() { var message = 'this is my message'; - var plaintext = new dcodeIO.ByteBuffer.wrap(message, 'binary').toArrayBuffer(); + var plaintext = new dcodeIO.ByteBuffer.wrap( + message, + 'binary' + ).toArrayBuffer(); var key = textsecure.crypto.getRandomBytes(32); var encrypted = await Signal.Crypto.encryptSymmetric(key, plaintext); @@ -69,7 +87,10 @@ describe('Crypto', function() { uintArray[35] = 9; try { - var decrypted = await Signal.Crypto.decryptSymmetric(key, uintArray.buffer); + var decrypted = await Signal.Crypto.decryptSymmetric( + key, + uintArray.buffer + ); } catch (error) { assert.strictEqual( error.message, diff --git a/test/emoji_util_test.js b/test/emoji_util_test.js index 3757b086b..681adedce 100644 --- a/test/emoji_util_test.js +++ b/test/emoji_util_test.js @@ -1,170 +1,179 @@ 'use strict'; describe('EmojiUtil', function() { - describe('getCountOfAllMatches', function() { - it('returns zero for string with no matches', function() { - var r = /s/g; - var str = 'no match'; - var actual = emoji.getCountOfAllMatches(str, r); - assert.equal(actual, 0); - }); - it('returns 1 for one match', function() { - var r = /s/g; - var str = 'just one match'; - var actual = emoji.getCountOfAllMatches(str, r); - assert.equal(actual, 1); - }); - it('returns 2 for two matches', function() { - var r = /s/g; - var str = 's + s'; - var actual = emoji.getCountOfAllMatches(str, r); - assert.equal(actual, 2); - }); - it('returns zero for no match with non-global regular expression', function() { - var r = /s/g; - var str = 'no match'; - var actual = emoji.getCountOfAllMatches(str, r); - assert.equal(actual, 0); - }); - it('returns 1 for match with non-global regular expression', function() { - var r = /s/; - var str = 's + s'; - var actual = emoji.getCountOfAllMatches(str, r); - assert.equal(actual, 1); - }); + describe('getCountOfAllMatches', function() { + it('returns zero for string with no matches', function() { + var r = /s/g; + var str = 'no match'; + var actual = emoji.getCountOfAllMatches(str, r); + assert.equal(actual, 0); + }); + it('returns 1 for one match', function() { + var r = /s/g; + var str = 'just one match'; + var actual = emoji.getCountOfAllMatches(str, r); + assert.equal(actual, 1); + }); + it('returns 2 for two matches', function() { + var r = /s/g; + var str = 's + s'; + var actual = emoji.getCountOfAllMatches(str, r); + assert.equal(actual, 2); + }); + it('returns zero for no match with non-global regular expression', function() { + var r = /s/g; + var str = 'no match'; + var actual = emoji.getCountOfAllMatches(str, r); + assert.equal(actual, 0); + }); + it('returns 1 for match with non-global regular expression', function() { + var r = /s/; + var str = 's + s'; + var actual = emoji.getCountOfAllMatches(str, r); + assert.equal(actual, 1); + }); + }); + + describe('hasNormalCharacters', function() { + it('returns true for all normal text', function() { + var str = 'normal'; + var actual = emoji.hasNormalCharacters(str); + assert.equal(actual, true); + }); + it('returns false for all emoji text', function() { + var str = '🔥🔥🔥🔥'; + var actual = emoji.hasNormalCharacters(str); + assert.equal(actual, false); + }); + it('returns false for emojis mixed with spaces', function() { + var str = '🔥 🔥 🔥 🔥'; + var actual = emoji.hasNormalCharacters(str); + assert.equal(actual, false); + }); + it('returns true for emojis and text', function() { + var str = '🔥 normal 🔥 🔥 🔥'; + var actual = emoji.hasNormalCharacters(str); + assert.equal(actual, true); + }); + }); + + describe('getSizeClass', function() { + it('returns nothing for non-emoji text', function() { + assert.equal(emoji.getSizeClass('normal text'), ''); + }); + it('returns nothing for emojis mixed with text', function() { + assert.equal(emoji.getSizeClass('🔥 normal 🔥'), ''); + }); + it('returns nothing for more than 8 emojis', function() { + assert.equal(emoji.getSizeClass('🔥🔥 🔥🔥 🔥🔥 🔥🔥 🔥'), ''); + }); + it('returns "small" for 7-8 emojis', function() { + assert.equal(emoji.getSizeClass('🔥🔥 🔥🔥 🔥🔥 🔥🔥'), 'small'); + assert.equal(emoji.getSizeClass('🔥🔥 🔥🔥 🔥🔥 🔥'), 'small'); + }); + it('returns "medium" for 5-6 emojis', function() { + assert.equal(emoji.getSizeClass('🔥🔥 🔥🔥 🔥🔥'), 'medium'); + assert.equal(emoji.getSizeClass('🔥🔥 🔥🔥 🔥'), 'medium'); + }); + it('returns "large" for 3-4 emojis', function() { + assert.equal(emoji.getSizeClass('🔥🔥 🔥🔥'), 'large'); + assert.equal(emoji.getSizeClass('🔥🔥 🔥'), 'large'); + }); + it('returns "jumbo" for 1-2 emojis', function() { + assert.equal(emoji.getSizeClass('🔥🔥'), 'jumbo'); + assert.equal(emoji.getSizeClass('🔥'), 'jumbo'); + }); + }); + + describe('addClass', function() { + it('returns original string if no emoji images', function() { + var start = 'no images. but there is some 🔥. '; + + var expected = start; + var actual = emoji.addClass(start, 'jumbo'); + + assert.equal(expected, actual); }); - describe('hasNormalCharacters', function() { - it('returns true for all normal text', function() { - var str = 'normal'; - var actual = emoji.hasNormalCharacters(str); - assert.equal(actual, true); - }); - it('returns false for all emoji text', function() { - var str = '🔥🔥🔥🔥'; - var actual = emoji.hasNormalCharacters(str); - assert.equal(actual, false); - }); - it('returns false for emojis mixed with spaces', function() { - var str = '🔥 🔥 🔥 🔥'; - var actual = emoji.hasNormalCharacters(str); - assert.equal(actual, false); - }); - it('returns true for emojis and text', function() { - var str = '🔥 normal 🔥 🔥 🔥'; - var actual = emoji.hasNormalCharacters(str); - assert.equal(actual, true); - }); + it('returns original string if no sizeClass provided', function() { + var start = + 'before after'; + + var expected = start; + var actual = emoji.addClass(start); + + assert.equal(expected, actual); }); - describe('getSizeClass', function() { - it('returns nothing for non-emoji text', function() { - assert.equal(emoji.getSizeClass('normal text'), ''); - }); - it('returns nothing for emojis mixed with text', function() { - assert.equal(emoji.getSizeClass('🔥 normal 🔥'), ''); - }); - it('returns nothing for more than 8 emojis', function() { - assert.equal(emoji.getSizeClass('🔥🔥 🔥🔥 🔥🔥 🔥🔥 🔥'), ''); - }); - it('returns "small" for 7-8 emojis', function() { - assert.equal(emoji.getSizeClass('🔥🔥 🔥🔥 🔥🔥 🔥🔥'), 'small'); - assert.equal(emoji.getSizeClass('🔥🔥 🔥🔥 🔥🔥 🔥'), 'small'); - }); - it('returns "medium" for 5-6 emojis', function() { - assert.equal(emoji.getSizeClass('🔥🔥 🔥🔥 🔥🔥'), 'medium'); - assert.equal(emoji.getSizeClass('🔥🔥 🔥🔥 🔥'), 'medium'); - }); - it('returns "large" for 3-4 emojis', function() { - assert.equal(emoji.getSizeClass('🔥🔥 🔥🔥'), 'large'); - assert.equal(emoji.getSizeClass('🔥🔥 🔥'), 'large'); - }); - it('returns "jumbo" for 1-2 emojis', function() { - assert.equal(emoji.getSizeClass('🔥🔥'), 'jumbo'); - assert.equal(emoji.getSizeClass('🔥'), 'jumbo'); - }); + it('adds provided class to image class', function() { + var start = + 'before after'; + + var expected = + 'before after'; + var actual = emoji.addClass(start, 'jumbo'); + + assert.equal(expected, actual); + }); + }); + + describe('ensureTitlesHaveColons', function() { + it('returns original string if no emoji images', function() { + var start = 'no images. but there is some 🔥. '; + + var expected = start; + var actual = emoji.ensureTitlesHaveColons(start); + + assert.equal(expected, actual); }); - describe('addClass', function() { - it('returns original string if no emoji images', function() { - var start = 'no images. but there is some 🔥. '; + it('returns original string if image title already has colons', function() { + var start = + 'before after'; - var expected = start; - var actual = emoji.addClass(start, 'jumbo'); + var expected = start; + var actual = emoji.ensureTitlesHaveColons(start); - assert.equal(expected, actual); - }); - - it('returns original string if no sizeClass provided', function() { - var start = 'before after'; - - var expected = start; - var actual = emoji.addClass(start); - - assert.equal(expected, actual); - }); - - it('adds provided class to image class', function() { - var start = 'before after'; - - var expected = 'before after'; - var actual = emoji.addClass(start, 'jumbo'); - - assert.equal(expected, actual); - }); + assert.equal(expected, actual); }); - describe('ensureTitlesHaveColons', function() { - it('returns original string if no emoji images', function() { - var start = 'no images. but there is some 🔥. '; + it('does not change title for non-emoji image', function() { + var start = + 'before after'; - var expected = start; - var actual = emoji.ensureTitlesHaveColons(start); + var expected = start; + var actual = emoji.ensureTitlesHaveColons(start); - assert.equal(expected, actual); - }); - - it('returns original string if image title already has colons', function() { - var start = 'before after'; - - var expected = start; - var actual = emoji.ensureTitlesHaveColons(start); - - assert.equal(expected, actual); - }); - - it('does not change title for non-emoji image', function() { - var start = 'before after'; - - var expected = start; - var actual = emoji.ensureTitlesHaveColons(start); - - assert.equal(expected, actual); - }); - - it('adds colons to emoji image title', function() { - var start = 'before after'; - - var expected = 'before after'; - var actual = emoji.ensureTitlesHaveColons(start); - - assert.equal(expected, actual); - }); + assert.equal(expected, actual); }); - describe('signalReplace', function() { - it('returns images for every emoji', function() { - var actual = emoji.signalReplace('🏠 🔥'); - var expected = '' - + ' '; + it('adds colons to emoji image title', function() { + var start = + 'before after'; - assert.equal(expected, actual); - }); - it('properly hyphenates a variation', function() { - var actual = emoji.signalReplace('💪🏿'); // muscle with dark skin tone modifier - var expected = ''; + var expected = + 'before after'; + var actual = emoji.ensureTitlesHaveColons(start); - assert.equal(expected, actual); - }); + assert.equal(expected, actual); }); + }); + + describe('signalReplace', function() { + it('returns images for every emoji', function() { + var actual = emoji.signalReplace('🏠 🔥'); + var expected = + '' + + ' '; + + assert.equal(expected, actual); + }); + it('properly hyphenates a variation', function() { + var actual = emoji.signalReplace('💪🏿'); // muscle with dark skin tone modifier + var expected = + ''; + + assert.equal(expected, actual); + }); + }); }); diff --git a/test/fixtures.js b/test/fixtures.js index 956492397..aafcdaae9 100644 --- a/test/fixtures.js +++ b/test/fixtures.js @@ -1,2688 +1,2709 @@ -Whisper.Fixtures = (function() { - var VERA_ID = '+13016886524'; // nsa - var NESTOR_ID = '+17034820623'; // cia - var MASHA_ID = '+441242221491'; // gchq - var FRED_ID = '+14155537400'; // fbi sf - var MICHEL_ID = '+12024561111'; // twh +Whisper.Fixtures = function() { + var VERA_ID = '+13016886524'; // nsa + var NESTOR_ID = '+17034820623'; // cia + var MASHA_ID = '+441242221491'; // gchq + var FRED_ID = '+14155537400'; // fbi sf + var MICHEL_ID = '+12024561111'; // twh - var now = Date.now(); - var conversationCollection = new Whisper.ConversationCollection([ - { - name: 'Vera Zasulich', - id: VERA_ID, - type: 'private', - active_at: now - 10000000, - timestamp: now - 10000000, - lastMessage: "My piece for Iskra is ready!" - }, - { - name: 'Nestor Mahkno', - type: 'private', - active_at: now - 1000000000, - timestamp: now - 1000000000, - id: NESTOR_ID, - lastMessage: "Need a ride?" - }, - { - name: 'Friedrich Nietzsche', - type: 'private', - active_at: now - 500000000, - timestamp: now - 500000000, - id: FRED_ID, - lastMessage: "It is my ambition to say in ten sentences what everyone else says in a whole book — what everyone else does not say in a whole book.", - }, - { - name: 'Masha Kolenkina', - type: 'private', - active_at: now - 1, - timestamp: now - 1, - id: MASHA_ID, // gchq - lastMessage: "I can't wait to try it!", - unreadCount: 1 - }, - { - id: MICHEL_ID, - name: 'Michel Foucault', - type: 'private', - active_at: now - 1000000, - timestamp: now - 1000000, - lastMessage: "The soul is the prison of the body." - } - ]); + var now = Date.now(); + var conversationCollection = new Whisper.ConversationCollection([ + { + name: 'Vera Zasulich', + id: VERA_ID, + type: 'private', + active_at: now - 10000000, + timestamp: now - 10000000, + lastMessage: 'My piece for Iskra is ready!', + }, + { + name: 'Nestor Mahkno', + type: 'private', + active_at: now - 1000000000, + timestamp: now - 1000000000, + id: NESTOR_ID, + lastMessage: 'Need a ride?', + }, + { + name: 'Friedrich Nietzsche', + type: 'private', + active_at: now - 500000000, + timestamp: now - 500000000, + id: FRED_ID, + lastMessage: + 'It is my ambition to say in ten sentences what everyone else says in a whole book — what everyone else does not say in a whole book.', + }, + { + name: 'Masha Kolenkina', + type: 'private', + active_at: now - 1, + timestamp: now - 1, + id: MASHA_ID, // gchq + lastMessage: "I can't wait to try it!", + unreadCount: 1, + }, + { + id: MICHEL_ID, + name: 'Michel Foucault', + type: 'private', + active_at: now - 1000000, + timestamp: now - 1000000, + lastMessage: 'The soul is the prison of the body.', + }, + ]); - var Vera = conversationCollection.get(VERA_ID); - Vera.messageCollection.add([ - { - conversationId: VERA_ID, - type: 'incoming', - sent_at: now - 10000000, - received_at: now - 10000000, - body: "My piece for Iskra is ready!", - } - ]); + var Vera = conversationCollection.get(VERA_ID); + Vera.messageCollection.add([ + { + conversationId: VERA_ID, + type: 'incoming', + sent_at: now - 10000000, + received_at: now - 10000000, + body: 'My piece for Iskra is ready!', + }, + ]); - var Nestor = conversationCollection.get(NESTOR_ID); - Nestor.messageCollection.add([ - { - conversationId: NESTOR_ID, - type: 'incoming', - sent_at: now - 1000000000, - received_at: now - 1000000000, - body: "Need a ride?", - } - ]); + var Nestor = conversationCollection.get(NESTOR_ID); + Nestor.messageCollection.add([ + { + conversationId: NESTOR_ID, + type: 'incoming', + sent_at: now - 1000000000, + received_at: now - 1000000000, + body: 'Need a ride?', + }, + ]); - var Fred = conversationCollection.get(FRED_ID); - Fred.messageCollection.add([ - { - conversationId: FRED_ID, - type: 'incoming', - sent_at: now - 500000000, - received_at: now - 500000000, - body: "It is my ambition to say in ten sentences what everyone else says in a whole book — what everyone else does not say in a whole book.", - } - ]); + var Fred = conversationCollection.get(FRED_ID); + Fred.messageCollection.add([ + { + conversationId: FRED_ID, + type: 'incoming', + sent_at: now - 500000000, + received_at: now - 500000000, + body: + 'It is my ambition to say in ten sentences what everyone else says in a whole book — what everyone else does not say in a whole book.', + }, + ]); - var Michel = conversationCollection.get(MICHEL_ID); - Michel.messageCollection.add([ + var Michel = conversationCollection.get(MICHEL_ID); + Michel.messageCollection.add([ + { + conversationId: MICHEL_ID, + type: 'outgoing', + sent_at: now - 3000000, + received_at: now - 3000000, + attachments: [ { - conversationId: MICHEL_ID, - type: 'outgoing', - sent_at: now - 3000000, - received_at: now - 3000000, - attachments: [{ - contentType: 'image/jpeg', - data: dataURItoBlob(getImage1()) - }] + contentType: 'image/jpeg', + data: dataURItoBlob(getImage1()), }, - { - conversationId: MICHEL_ID, - type: 'incoming', - body: "The soul is the prison of the body.", - sent_at: now - 1000000, - received_at: now - 1000000, - source: MICHEL_ID, - }, - { - conversationId: MICHEL_ID, - type: 'incoming', - sent_at: now - 2000000, - body: "In their opinion I am a dangerous man, since I am a crypto-Marxist.", - received_at: now - 2000000, - source: MICHEL_ID, - }, - ]); + ], + }, + { + conversationId: MICHEL_ID, + type: 'incoming', + body: 'The soul is the prison of the body.', + sent_at: now - 1000000, + received_at: now - 1000000, + source: MICHEL_ID, + }, + { + conversationId: MICHEL_ID, + type: 'incoming', + sent_at: now - 2000000, + body: + 'In their opinion I am a dangerous man, since I am a crypto-Marxist.', + received_at: now - 2000000, + source: MICHEL_ID, + }, + ]); - var Masha = conversationCollection.get(MASHA_ID); - Masha.messageCollection.add([ - { - date: now - 60000, - type: 'incoming', - source: MASHA_ID, - attachments: [{ - contentType: 'image/jpeg', - data: dataURItoBlob(getImage2()) - }] - }, - { - date: now - 5000, - type: 'outgoing', - body: "I just installed Signal Desktop!" - }, - { - date: now - 4000, - type: 'outgoing', - body: "Now I can use Signal on my computer, even when my phone is off. 😄 " - }, - { - date: now, - type: 'incoming', - source: MASHA_ID, - body: "I can't wait to try it!", - unread: 1 - }, + var Masha = conversationCollection.get(MASHA_ID); + Masha.messageCollection.add( + [ + { + date: now - 60000, + type: 'incoming', + source: MASHA_ID, + attachments: [ + { + contentType: 'image/jpeg', + data: dataURItoBlob(getImage2()), + }, + ], + }, + { + date: now - 5000, + type: 'outgoing', + body: 'I just installed Signal Desktop!', + }, + { + date: now - 4000, + type: 'outgoing', + body: + 'Now I can use Signal on my computer, even when my phone is off. 😄 ', + }, + { + date: now, + type: 'incoming', + source: MASHA_ID, + body: "I can't wait to try it!", + unread: 1, + }, ].map(function(m) { - return { - conversationId : MASHA_ID, - type : m.type, - body : m.body, - sent_at : m.date, - received_at : m.date, - timestamp : m.date, - attachments : m.attachments, - unread : m.unread, - source : m.source, - }; - })); + return { + conversationId: MASHA_ID, + type: m.type, + body: m.body, + sent_at: m.date, + received_at: m.date, + timestamp: m.date, + attachments: m.attachments, + unread: m.unread, + source: m.source, + }; + }) + ); - var group = conversationCollection.add({ - name: '📖 Book Club', - type: 'group', - active_at: now - 100000, - timestamp: now - 100000, - id: 'group1', - lastMessage: "See you all there!", - members: [MICHEL_ID, FRED_ID, NESTOR_ID], - }); - group.messageCollection.add([ - { - date: now - 60 * 1000 * 30, - type: 'incoming', - body: "If you knew when you began a book what you would say at the end, do you think that you would have the courage to write it?", - source: MICHEL_ID, - }, - { - date: now - 60 * 1000 * 20, - type: 'incoming', - body: "A book which has a strange knack of seeking out its fellow-revellers and enticing them on to new secret paths and dancing-places.", - source: FRED_ID - }, - { - date: now - 60 * 1000 * 4, - type: 'incoming', - body: "Let's meet in the library.", - source: NESTOR_ID - }, - { - date: now - 60 * 1000, - type: 'incoming', - body: "I'll be a little late.", - source: NESTOR_ID - }, - { - date: now, - type: 'outgoing', - body: "See you all there!", - recipients: [MICHEL_ID, FRED_ID, NESTOR_ID], - delivered_to: [MICHEL_ID, FRED_ID], - sent_to: [NESTOR_ID], - }, + var group = conversationCollection.add({ + name: '📖 Book Club', + type: 'group', + active_at: now - 100000, + timestamp: now - 100000, + id: 'group1', + lastMessage: 'See you all there!', + members: [MICHEL_ID, FRED_ID, NESTOR_ID], + }); + group.messageCollection.add( + [ + { + date: now - 60 * 1000 * 30, + type: 'incoming', + body: + 'If you knew when you began a book what you would say at the end, do you think that you would have the courage to write it?', + source: MICHEL_ID, + }, + { + date: now - 60 * 1000 * 20, + type: 'incoming', + body: + 'A book which has a strange knack of seeking out its fellow-revellers and enticing them on to new secret paths and dancing-places.', + source: FRED_ID, + }, + { + date: now - 60 * 1000 * 4, + type: 'incoming', + body: "Let's meet in the library.", + source: NESTOR_ID, + }, + { + date: now - 60 * 1000, + type: 'incoming', + body: "I'll be a little late.", + source: NESTOR_ID, + }, + { + date: now, + type: 'outgoing', + body: 'See you all there!', + recipients: [MICHEL_ID, FRED_ID, NESTOR_ID], + delivered_to: [MICHEL_ID, FRED_ID], + sent_to: [NESTOR_ID], + }, ].map(function(m) { - return Object.assign({}, m, { - conversationId : group.id, - sent_at : m.date, - received_at : m.date, - timestamp : m.date, + return Object.assign({}, m, { + conversationId: group.id, + sent_at: m.date, + received_at: m.date, + timestamp: m.date, + }); + }) + ); + + function dataURItoBlob(dataURI) { + var binary = atob(dataURI.split(',')[1]); + var array = []; + for (var i = 0; i < binary.length; i++) { + array.push(binary.charCodeAt(i)); + } + return new Uint8Array(array).buffer; + } + + conversationCollection.saveAll = function() { + return Promise.all( + this.map(function(convo) { + return new Promise(function(resolve) { + convo.save().then(resolve); + }).then(function() { + return Promise.all( + convo.messageCollection.map(function(message) { + return new Promise(function(resolve) { + message.save().then(resolve); + }); + }) + ); }); - })); + }) + ); + }; - function dataURItoBlob(dataURI) { - var binary = atob(dataURI.split(',')[1]); - var array = []; - for (var i = 0; i < binary.length; i++) { - array.push(binary.charCodeAt(i)); - } - return new Uint8Array(array).buffer; - } + function getImage1() { + return ( + 'data:image/jpeg;base64,' + + '/9j/4QAqRXhpZgAASUkqAAgAAAABAJiCAgAFAAAAGgAAAAAAAAAgICAgAAAAAP/sABFEdWNreQAB' + + 'AAQAAAA8AAD/4QOdaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49' + + 'Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9' + + 'ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2' + + 'NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0' + + 'cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRp' + + 'b24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAv' + + 'bW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291' + + 'cmNlUmVmIyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpk' + + 'Yz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtcE1NOkRvY3VtZW50SUQ9Inht' + + 'cC5kaWQ6NEEzMzkwN0E5OUQzMTFFNEIzRDI4QTFBM0M2Mzk4MzIiIHhtcE1NOkluc3RhbmNlSUQ9' + + 'InhtcC5paWQ6NEEzMzkwNzk5OUQzMTFFNEIzRDI4QTFBM0M2Mzk4MzIiIHhtcDpDcmVhdG9yVG9v' + + 'bD0iRGlnaXRhbCBDYW1lcmEgWDIwIFZlcjEuMDIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6' + + 'aW5zdGFuY2VJRD0iNzM3N0Y3RjI0MTU4RDg0M0M1QUIzMzY2QzcwOTM0NTQiIHN0UmVmOmRvY3Vt' + + 'ZW50SUQ9IjczNzdGN0YyNDE1OEQ4NDNDNUFCMzM2NkM3MDkzNDU0Ii8+IDxkYzpyaWdodHM+IDxy' + + 'ZGY6QWx0PiA8cmRmOmxpIHhtbDpsYW5nPSJ4LWRlZmF1bHQiPiAgICA8L3JkZjpsaT4gPC9yZGY6' + + 'QWx0PiA8L2RjOnJpZ2h0cz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1l' + + 'dGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7QBQUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAABgcAVoA' + + 'AxslRxwCAAACAAIcAnQABCAgICA4QklNBCUAAAAAABCF+g4zyeKkyI/MF2pb7RQW/+4ADkFkb2Jl' + + 'AGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoKDBAMDAwMDAwQDA4PEA8ODBMT' + + 'FBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8fHx8fHx8fHx8fHx8fHx8fHx8f' + + 'Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgB4AKAAwERAAIRAQMRAf/EAKIAAAIDAQEB' + + 'AQAAAAAAAAAAAAIDAAEEBQYHCAEBAQEBAQEBAAAAAAAAAAAAAAECAwQFBhAAAgEDAgQEBAQFAgYB' + + 'AgUFAQIAEQMEIRIxQVEFYXEiE4GRMgahsUIUwdFSIwfw4fFicoIzFSSiFpKywmNzgzREZCURAQEB' + + 'AQACAgICAgIBBAIDAAABEQISAyExQRNRBGEicTKB8JGhFLFC4SMz/9oADAMBAAIRAxEAPwD8rwJA' + + 'JSRwkUalgarxEg6OHYu3Vdzkpj6EDexFRxPATFv+G5G3tVjs/ru5We6eyQFQEobnL0FQ7fhJ1Kc2' + + 'ORl2xayXVN3t7j7bOCCy10JrNsqAPMU85FEDoB0NYGnE3W7gYNsBPqNKgCZqx11yMK29vKtkX8g1' + + 'W3YZN6gUobjAabq6qOU5+N+r9N7/AA939q/fuRbRrPd7rXrIUC3iqgAIZqeoc6acJ5++P4+nTm/z' + + '9ux3PC7Rk5TZlyzex1KblzfbZrT3EFKHVqHiATOfzZmtbny9b2vMHcex2bqAfubdylpQN7NQCgct' + + 't2OjUIas58+vfhu9Y9Z2LNvX8G6HVkdHZgtFVrgDU3KtdJvn4qX5juox/ZJdQsjDWjAV16iXdqWY' + + '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' + ); + } - conversationCollection.saveAll = function() { - return Promise.all(this.map(function(convo) { - return new Promise(function(resolve) { - convo.save().then(resolve); - }).then(function() { - return Promise.all(convo.messageCollection.map(function(message) { - return new Promise(function(resolve) { - message.save().then(resolve); - }); - })); - }); - })); - }; - - function getImage1() { - return "data:image/jpeg;base64," + - "/9j/4QAqRXhpZgAASUkqAAgAAAABAJiCAgAFAAAAGgAAAAAAAAAgICAgAAAAAP/sABFEdWNreQAB" + - "AAQAAAA8AAD/4QOdaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49" + - "Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9" + - "ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjMtYzAxMSA2Ni4xNDU2" + - "NjEsIDIwMTIvMDIvMDYtMTQ6NTY6MjcgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0" + - "cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRp" + - "b24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAv" + - "bW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291" + - "cmNlUmVmIyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpk" + - "Yz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtcE1NOkRvY3VtZW50SUQ9Inht" + - "cC5kaWQ6NEEzMzkwN0E5OUQzMTFFNEIzRDI4QTFBM0M2Mzk4MzIiIHhtcE1NOkluc3RhbmNlSUQ9" + - "InhtcC5paWQ6NEEzMzkwNzk5OUQzMTFFNEIzRDI4QTFBM0M2Mzk4MzIiIHhtcDpDcmVhdG9yVG9v" + - "bD0iRGlnaXRhbCBDYW1lcmEgWDIwIFZlcjEuMDIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6" + - "aW5zdGFuY2VJRD0iNzM3N0Y3RjI0MTU4RDg0M0M1QUIzMzY2QzcwOTM0NTQiIHN0UmVmOmRvY3Vt" + - "ZW50SUQ9IjczNzdGN0YyNDE1OEQ4NDNDNUFCMzM2NkM3MDkzNDU0Ii8+IDxkYzpyaWdodHM+IDxy" + - "ZGY6QWx0PiA8cmRmOmxpIHhtbDpsYW5nPSJ4LWRlZmF1bHQiPiAgICA8L3JkZjpsaT4gPC9yZGY6" + - "QWx0PiA8L2RjOnJpZ2h0cz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1l" + - "dGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7QBQUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAABgcAVoA" + - "AxslRxwCAAACAAIcAnQABCAgICA4QklNBCUAAAAAABCF+g4zyeKkyI/MF2pb7RQW/+4ADkFkb2Jl" + - "AGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoKDBAMDAwMDAwQDA4PEA8ODBMT" + - "FBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8fHx8fHx8fHx8fHx8fHx8fHx8f" + - "Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgB4AKAAwERAAIRAQMRAf/EAKIAAAIDAQEB" + - "AQAAAAAAAAAAAAIDAAEEBQYHCAEBAQEBAQEBAAAAAAAAAAAAAAECAwQFBhAAAgEDAgQEBAQFAgYB" + - "AgUFAQIAEQMEIRIxQVEFYXEiE4GRMgahsUIUwdFSIwfw4fFicoIzFSSiFpKywmNzgzREZCURAQEB" + - "AQACAgICAgIBBAIDAAABEQISAyExQRNRBGEicTKB8JGhFLFC4SMz/9oADAMBAAIRAxEAPwD8rwJA" + - "JSRwkUalgarxEg6OHYu3Vdzkpj6EDexFRxPATFv+G5G3tVjs/ru5We6eyQFQEobnL0FQ7fhJ1Kc2" + - "ORl2xayXVN3t7j7bOCCy10JrNsqAPMU85FEDoB0NYGnE3W7gYNsBPqNKgCZqx11yMK29vKtkX8g1" + - "W3YZN6gUobjAabq6qOU5+N+r9N7/AA939q/fuRbRrPd7rXrIUC3iqgAIZqeoc6acJ5++P4+nTm/z" + - "9ux3PC7Rk5TZlyzex1KblzfbZrT3EFKHVqHiATOfzZmtbny9b2vMHcex2bqAfubdylpQN7NQCgct" + - "t2OjUIas58+vfhu9Y9Z2LNvX8G6HVkdHZgtFVrgDU3KtdJvn4qX5juox/ZJdQsjDWjAV16iXdqWY" + - "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"; - }; - - function getImage2() { - return "data:image/jpeg;base64," + - "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsK" + - "CwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQU" + - "FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAKAAoADASIA" + - "AhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA" + - "AAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3" + - "ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm" + - "p6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA" + - "AwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx" + - "BhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK" + - "U1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3" + - "uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDzqMFh" + - "lRyOT16Z5/kacsQkYADAIzxzj3qeOEkhyOCMY7gj/wDXT41wig8DoR3r97btqj+aFIreXg7up65O" + - "evcfqPSlMW7G1ec9umOvXP1q+AAx+UMjcfL1I69f1qNogMAA/J9cZ9h/hUqd9yW+5VRGcDqcn1yO" + - "+cUnk72O3Gck887vx+uatYGdgUKT2XHr1HenKuw88EdBjv61XMHN2I4bRnCn5S5OcHkY9vwzW9DB" + - "HHAi7QT0PHas1SN55yvXP+e1alvIQgyrYHOQff8AD2rirtsuEk9SaBTDKoHO/ueB7/pXW6DY+VcL" + - "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"; - } - - return conversationCollection; -}); + function getImage2() { + return ( + 'data:image/jpeg;base64,' + + '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsK' + + 'CwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQU' + + 'FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAKAAoADASIA' + + 'AhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA' + + 'AAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3' + + 'ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm' + + 'p6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA' + + 'AwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx' + + 'BhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK' + + 'U1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3' + + 'uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDzqMFh' + + 'lRyOT16Z5/kacsQkYADAIzxzj3qeOEkhyOCMY7gj/wDXT41wig8DoR3r97btqj+aFIreXg7up65O' + + 'evcfqPSlMW7G1ec9umOvXP1q+AAx+UMjcfL1I69f1qNogMAA/J9cZ9h/hUqd9yW+5VRGcDqcn1yO' + + '+cUnk72O3Gck887vx+uatYGdgUKT2XHr1HenKuw88EdBjv61XMHN2I4bRnCn5S5OcHkY9vwzW9DB' + + 'HHAi7QT0PHas1SN55yvXP+e1alvIQgyrYHOQff8AD2rirtsuEk9SaBTDKoHO/ueB7/pXW6DY+VcL' + + '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' + ); + } + return conversationCollection; +}; diff --git a/test/fixtures_test.js b/test/fixtures_test.js index 29adb6b3c..42611d537 100644 --- a/test/fixtures_test.js +++ b/test/fixtures_test.js @@ -1,31 +1,37 @@ 'use strict'; -describe("Fixtures", function() { +describe('Fixtures', function() { before(function(done) { // NetworkStatusView checks this method every five seconds while showing - window.getSocketStatus = function() { return WebSocket.OPEN; }; + window.getSocketStatus = function() { + return WebSocket.OPEN; + }; - Whisper.Fixtures().saveAll().then(function() { - done(); - }); + Whisper.Fixtures() + .saveAll() + .then(function() { + done(); + }); }); it('renders', function(done) { ConversationController.reset(); - ConversationController.load().then(function() { - var view = new Whisper.InboxView({window: window}); - view.onEmpty(); - view.$el.prependTo($('#render-android')); + ConversationController.load() + .then(function() { + var view = new Whisper.InboxView({ window: window }); + view.onEmpty(); + view.$el.prependTo($('#render-android')); - var view = new Whisper.InboxView({window: window}); - view.$el.removeClass('android').addClass('ios'); - view.onEmpty(); - view.$el.prependTo($('#render-ios')); + var view = new Whisper.InboxView({ window: window }); + view.$el.removeClass('android').addClass('ios'); + view.onEmpty(); + view.$el.prependTo($('#render-ios')); - var view = new Whisper.InboxView({window: window}); - view.$el.removeClass('android').addClass('android-dark'); - view.onEmpty(); - view.$el.prependTo($('#render-android-dark')); - }).then(done, done); + var view = new Whisper.InboxView({ window: window }); + view.$el.removeClass('android').addClass('android-dark'); + view.onEmpty(); + view.$el.prependTo($('#render-android-dark')); + }) + .then(done, done); }); }); diff --git a/test/i18n_test.js b/test/i18n_test.js index 6c094487d..f992d0ab1 100644 --- a/test/i18n_test.js +++ b/test/i18n_test.js @@ -12,7 +12,10 @@ describe('i18n', function() { }); it('returns message with multiple substitutions', function() { const actual = i18n('verifyContact', ['', '']); - assert.equal(actual, 'You may wish to verify your safety number with this contact.'); + assert.equal( + actual, + 'You may wish to verify your safety number with this contact.' + ); }); }); diff --git a/test/keychange_listener_test.js b/test/keychange_listener_test.js index 055a8bb7f..6b22a3054 100644 --- a/test/keychange_listener_test.js +++ b/test/keychange_listener_test.js @@ -1,6 +1,9 @@ describe('KeyChangeListener', function() { - var phoneNumberWithKeyChange = '+13016886524'; // nsa - var address = new libsignal.SignalProtocolAddress(phoneNumberWithKeyChange, 1); + var phoneNumberWithKeyChange = '+13016886524'; // nsa + var address = new libsignal.SignalProtocolAddress( + phoneNumberWithKeyChange, + 1 + ); var oldKey = libsignal.crypto.getRandomBytes(33); var newKey = libsignal.crypto.getRandomBytes(33); var store; @@ -42,7 +45,6 @@ describe('KeyChangeListener', function() { }); }); - describe('When we have a group with this contact', function() { let convo; before(function() { @@ -68,7 +70,5 @@ describe('KeyChangeListener', function() { }); return store.saveIdentity(address.toString(), newKey); }); - }); - }); diff --git a/test/libphonenumber_util_test.js b/test/libphonenumber_util_test.js index 9e98bead8..5140da974 100644 --- a/test/libphonenumber_util_test.js +++ b/test/libphonenumber_util_test.js @@ -2,28 +2,28 @@ * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - describe('libphonenumber util', function() { - describe('parseNumber', function() { - it('numbers with + are valid without providing regionCode', function() { - var result = libphonenumber.util.parseNumber('+14155555555'); - assert.isTrue(result.isValidNumber); - assert.strictEqual(result.nationalNumber, '4155555555'); - assert.strictEqual(result.e164, '+14155555555'); - assert.strictEqual(result.regionCode, 'US'); - assert.strictEqual(result.countryCode, '1'); - }); - it('variant numbers with the right regionCode are valid', function() { - [ '4155555555', '14155555555', '+14155555555', ].forEach(function(number) { - var result = libphonenumber.util.parseNumber(number, 'US'); - assert.isTrue(result.isValidNumber); - assert.strictEqual(result.nationalNumber, '4155555555'); - assert.strictEqual(result.e164, '+14155555555'); - assert.strictEqual(result.regionCode, 'US'); - assert.strictEqual(result.countryCode, '1'); - }); - }); +(function() { + 'use strict'; + describe('libphonenumber util', function() { + describe('parseNumber', function() { + it('numbers with + are valid without providing regionCode', function() { + var result = libphonenumber.util.parseNumber('+14155555555'); + assert.isTrue(result.isValidNumber); + assert.strictEqual(result.nationalNumber, '4155555555'); + assert.strictEqual(result.e164, '+14155555555'); + assert.strictEqual(result.regionCode, 'US'); + assert.strictEqual(result.countryCode, '1'); + }); + it('variant numbers with the right regionCode are valid', function() { + ['4155555555', '14155555555', '+14155555555'].forEach(function(number) { + var result = libphonenumber.util.parseNumber(number, 'US'); + assert.isTrue(result.isValidNumber); + assert.strictEqual(result.nationalNumber, '4155555555'); + assert.strictEqual(result.e164, '+14155555555'); + assert.strictEqual(result.regionCode, 'US'); + assert.strictEqual(result.countryCode, '1'); }); + }); }); + }); })(); diff --git a/test/models/conversations_test.js b/test/models/conversations_test.js index 620a0bd6e..fd13281bf 100644 --- a/test/models/conversations_test.js +++ b/test/models/conversations_test.js @@ -2,248 +2,285 @@ * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - var attributes = { type: 'outgoing', - body: 'hi', - conversationId: 'foo', - attachments: [], - timestamp: new Date().getTime() }; - var conversation_attributes= { - type: 'private', - id: '+14155555555' - }; - textsecure.messaging = new textsecure.MessageSender(''); +(function() { + 'use strict'; + var attributes = { + type: 'outgoing', + body: 'hi', + conversationId: 'foo', + attachments: [], + timestamp: new Date().getTime(), + }; + var conversation_attributes = { + type: 'private', + id: '+14155555555', + }; + textsecure.messaging = new textsecure.MessageSender(''); - describe('ConversationCollection', function() { - before(clearDatabase); - after(clearDatabase); + describe('ConversationCollection', function() { + before(clearDatabase); + after(clearDatabase); - it('adds without saving', function(done) { + it('adds without saving', function(done) { + var convos = new Whisper.ConversationCollection(); + convos.add(conversation_attributes); + assert.notEqual(convos.length, 0); - var convos = new Whisper.ConversationCollection(); - convos.add(conversation_attributes); - assert.notEqual(convos.length, 0); + var convos = new Whisper.ConversationCollection(); + convos.fetch().then(function() { + assert.strictEqual(convos.length, 0); + done(); + }); + }); - var convos = new Whisper.ConversationCollection(); - convos.fetch().then(function() { - assert.strictEqual(convos.length, 0); - done(); - }); + it('saves asynchronously', function(done) { + new Whisper.ConversationCollection() + .add(conversation_attributes) + .save() + .then(done); + }); + + it('fetches persistent convos', function(done) { + var convos = new Whisper.ConversationCollection(); + assert.strictEqual(convos.length, 0); + convos.fetch().then(function() { + var m = convos.at(0).attributes; + _.each(conversation_attributes, function(val, key) { + assert.deepEqual(m[key], val); }); + done(); + }); + }); - it('saves asynchronously', function(done) { - new Whisper.ConversationCollection().add(conversation_attributes).save().then(done); - }); - - it('fetches persistent convos', function(done) { - var convos = new Whisper.ConversationCollection(); + it('destroys persistent convos', function(done) { + var convos = new Whisper.ConversationCollection(); + convos.fetch().then(function() { + convos.destroyAll().then(function() { + var convos = new Whisper.ConversationCollection(); + convos.fetch().then(function() { assert.strictEqual(convos.length, 0); - convos.fetch().then(function() { - var m = convos.at(0).attributes; - _.each(conversation_attributes, function(val, key) { - assert.deepEqual(m[key], val); - }); - done(); - }); - }); - - it('destroys persistent convos', function(done) { - var convos = new Whisper.ConversationCollection(); - convos.fetch().then(function() { - convos.destroyAll().then(function() { - var convos = new Whisper.ConversationCollection(); - convos.fetch().then(function() { - assert.strictEqual(convos.length, 0); - done(); - }); - }); - }); - }); - - it('should be ordered newest to oldest', function() { - var conversations = new Whisper.ConversationCollection(); - // Timestamps - var today = new Date(); - var tomorrow = new Date(); - tomorrow.setDate(today.getDate()+1); - - // Add convos - conversations.add({ timestamp: today }); - conversations.add({ timestamp: tomorrow }); - - var models = conversations.models; - var firstTimestamp = models[0].get('timestamp').getTime(); - var secondTimestamp = models[1].get('timestamp').getTime(); - - // Compare timestamps - assert(firstTimestamp > secondTimestamp); + done(); + }); }); + }); }); - describe('Conversation', function() { - var attributes = { type: 'private', id: '+18085555555' }; - before(function(done) { - var convo = new Whisper.ConversationCollection().add(attributes); - convo.save().then(function() { - var message = convo.messageCollection.add({ - body : 'hello world', - conversationId : convo.id, - type : 'outgoing', - sent_at : Date.now(), - received_at : Date.now() - }); - message.save().then(done); - }); + it('should be ordered newest to oldest', function() { + var conversations = new Whisper.ConversationCollection(); + // Timestamps + var today = new Date(); + var tomorrow = new Date(); + tomorrow.setDate(today.getDate() + 1); + + // Add convos + conversations.add({ timestamp: today }); + conversations.add({ timestamp: tomorrow }); + + var models = conversations.models; + var firstTimestamp = models[0].get('timestamp').getTime(); + var secondTimestamp = models[1].get('timestamp').getTime(); + + // Compare timestamps + assert(firstTimestamp > secondTimestamp); + }); + }); + + describe('Conversation', function() { + var attributes = { type: 'private', id: '+18085555555' }; + before(function(done) { + var convo = new Whisper.ConversationCollection().add(attributes); + convo.save().then(function() { + var message = convo.messageCollection.add({ + body: 'hello world', + conversationId: convo.id, + type: 'outgoing', + sent_at: Date.now(), + received_at: Date.now(), }); - after(clearDatabase); + message.save().then(done); + }); + }); + after(clearDatabase); - it('sorts its contacts in an intl-friendly way', function() { - var convo = new Whisper.Conversation({id: '+18085555555'}); - convo.contactCollection.add(new Whisper.Conversation({ - name: 'C' - })); - convo.contactCollection.add(new Whisper.Conversation({ - name: 'B' - })); - convo.contactCollection.add(new Whisper.Conversation({ - name: 'Á' - })); + it('sorts its contacts in an intl-friendly way', function() { + var convo = new Whisper.Conversation({ id: '+18085555555' }); + convo.contactCollection.add( + new Whisper.Conversation({ + name: 'C', + }) + ); + convo.contactCollection.add( + new Whisper.Conversation({ + name: 'B', + }) + ); + convo.contactCollection.add( + new Whisper.Conversation({ + name: 'Á', + }) + ); - assert.strictEqual(convo.contactCollection.at('0').get('name'), 'Á'); - assert.strictEqual(convo.contactCollection.at('1').get('name'), 'B'); - assert.strictEqual(convo.contactCollection.at('2').get('name'), 'C'); - }); - - it('contains its own messages', function (done) { - var convo = new Whisper.ConversationCollection().add({id: '+18085555555'}); - convo.fetchMessages().then(function() { - assert.notEqual(convo.messageCollection.length, 0); - done(); - }); - }); - - it('contains only its own messages', function (done) { - var convo = new Whisper.ConversationCollection().add({id: '+18085556666'}); - convo.fetchMessages().then(function() { - assert.strictEqual(convo.messageCollection.length, 0); - done(); - }); - }); - - it('adds conversation to message collection upon leaving group', function() { - var convo = new Whisper.ConversationCollection().add({type: 'group', id: 'a random string'}); - convo.leaveGroup(); - assert.notEqual(convo.messageCollection.length, 0); - }); - - it('has a title', function() { - var convos = new Whisper.ConversationCollection(); - var convo = convos.add(attributes); - assert.equal(convo.getTitle(), '+1 808-555-5555'); - - convo = convos.add({type: ''}); - assert.equal(convo.getTitle(), 'Unknown group'); - - convo = convos.add({name: 'name'}); - assert.equal(convo.getTitle(), 'name'); - }); - - it('returns the number', function() { - var convos = new Whisper.ConversationCollection(); - var convo = convos.add(attributes); - assert.equal(convo.getNumber(), '+1 808-555-5555'); - - convo = convos.add({type: ''}); - assert.equal(convo.getNumber(), ''); - }); - - it('has an avatar', function() { - var convo = new Whisper.ConversationCollection().add(attributes); - var avatar = convo.getAvatar(); - assert.property(avatar, 'content'); - assert.property(avatar, 'color'); - }); - - it('revokes the avatar URL', function() { - var convo = new Whisper.ConversationCollection().add(attributes); - convo.revokeAvatarUrl(); - assert.notOk(convo.avatarUrl); - }); - - describe('phone number parsing', function() { - after(function() { storage.remove('regionCode'); }); - function checkAttributes(number) { - var convo = new Whisper.ConversationCollection().add({type: 'private'}); - convo.set('id', number); - convo.validate(convo.attributes); - assert.strictEqual(convo.get('id'), '+14155555555', number); - } - it('processes the phone number when validating', function() { - [ '+14155555555', ].forEach(checkAttributes); - }); - it('defaults to the local regionCode', function() { - storage.put('regionCode', 'US'); - [ '14155555555', '4155555555' ].forEach(checkAttributes); - }); - it('works with common phone number formats', function() { - storage.put('regionCode', 'US'); - [ - '415 555 5555', - '415-555-5555', - '(415) 555 5555', - '(415) 555-5555', - '1 415 555 5555', - '1 415-555-5555', - '1 (415) 555 5555', - '1 (415) 555-5555', - '+1 415 555 5555', - '+1 415-555-5555', - '+1 (415) 555 5555', - '+1 (415) 555-5555', - - ].forEach(checkAttributes); - }); - }); + assert.strictEqual(convo.contactCollection.at('0').get('name'), 'Á'); + assert.strictEqual(convo.contactCollection.at('1').get('name'), 'B'); + assert.strictEqual(convo.contactCollection.at('2').get('name'), 'C'); }); - describe('Conversation search', function() { + it('contains its own messages', function(done) { + var convo = new Whisper.ConversationCollection().add({ + id: '+18085555555', + }); + convo.fetchMessages().then(function() { + assert.notEqual(convo.messageCollection.length, 0); + done(); + }); + }); + + it('contains only its own messages', function(done) { + var convo = new Whisper.ConversationCollection().add({ + id: '+18085556666', + }); + convo.fetchMessages().then(function() { + assert.strictEqual(convo.messageCollection.length, 0); + done(); + }); + }); + + it('adds conversation to message collection upon leaving group', function() { + var convo = new Whisper.ConversationCollection().add({ + type: 'group', + id: 'a random string', + }); + convo.leaveGroup(); + assert.notEqual(convo.messageCollection.length, 0); + }); + + it('has a title', function() { + var convos = new Whisper.ConversationCollection(); + var convo = convos.add(attributes); + assert.equal(convo.getTitle(), '+1 808-555-5555'); + + convo = convos.add({ type: '' }); + assert.equal(convo.getTitle(), 'Unknown group'); + + convo = convos.add({ name: 'name' }); + assert.equal(convo.getTitle(), 'name'); + }); + + it('returns the number', function() { + var convos = new Whisper.ConversationCollection(); + var convo = convos.add(attributes); + assert.equal(convo.getNumber(), '+1 808-555-5555'); + + convo = convos.add({ type: '' }); + assert.equal(convo.getNumber(), ''); + }); + + it('has an avatar', function() { + var convo = new Whisper.ConversationCollection().add(attributes); + var avatar = convo.getAvatar(); + assert.property(avatar, 'content'); + assert.property(avatar, 'color'); + }); + + it('revokes the avatar URL', function() { + var convo = new Whisper.ConversationCollection().add(attributes); + convo.revokeAvatarUrl(); + assert.notOk(convo.avatarUrl); + }); + + describe('phone number parsing', function() { + after(function() { + storage.remove('regionCode'); + }); + function checkAttributes(number) { var convo = new Whisper.ConversationCollection().add({ - id: '+14155555555', type: 'private', name: 'John Doe' - }); - before(function(done) { convo.save().then(done); }); - function testSearch(queries, done) { - return Promise.all(queries.map(function(query) { - var collection = new Whisper.ConversationCollection(); - return collection.search(query).then(function() { - assert.isDefined(collection.get(convo.id), 'no result for "' + query + '"'); - }).catch(done); - })).then(function() { - done(); - }); - } - it('matches by partial phone number', function(done) { - testSearch([ - '1', - '4', - '+1', - '415', - '4155', - '4155555555', - '14155555555', - '+14155555555', - ], done); - }); - it('matches by name', function(done) { - testSearch([ 'John', 'Doe', 'john', 'doe', 'John Doe', 'john doe' ], done); - }); - it('does not match +', function(done) { - var collection = new Whisper.ConversationCollection(); - return collection.search('+').then(function() { - assert.isUndefined(collection.get(convo.id), 'got result for "+"'); - done(); - }).catch(done); + type: 'private', }); + convo.set('id', number); + convo.validate(convo.attributes); + assert.strictEqual(convo.get('id'), '+14155555555', number); + } + it('processes the phone number when validating', function() { + ['+14155555555'].forEach(checkAttributes); + }); + it('defaults to the local regionCode', function() { + storage.put('regionCode', 'US'); + ['14155555555', '4155555555'].forEach(checkAttributes); + }); + it('works with common phone number formats', function() { + storage.put('regionCode', 'US'); + [ + '415 555 5555', + '415-555-5555', + '(415) 555 5555', + '(415) 555-5555', + '1 415 555 5555', + '1 415-555-5555', + '1 (415) 555 5555', + '1 (415) 555-5555', + '+1 415 555 5555', + '+1 415-555-5555', + '+1 (415) 555 5555', + '+1 (415) 555-5555', + ].forEach(checkAttributes); + }); }); + }); -})();; + describe('Conversation search', function() { + var convo = new Whisper.ConversationCollection().add({ + id: '+14155555555', + type: 'private', + name: 'John Doe', + }); + before(function(done) { + convo.save().then(done); + }); + function testSearch(queries, done) { + return Promise.all( + queries.map(function(query) { + var collection = new Whisper.ConversationCollection(); + return collection + .search(query) + .then(function() { + assert.isDefined( + collection.get(convo.id), + 'no result for "' + query + '"' + ); + }) + .catch(done); + }) + ).then(function() { + done(); + }); + } + it('matches by partial phone number', function(done) { + testSearch( + [ + '1', + '4', + '+1', + '415', + '4155', + '4155555555', + '14155555555', + '+14155555555', + ], + done + ); + }); + it('matches by name', function(done) { + testSearch(['John', 'Doe', 'john', 'doe', 'John Doe', 'john doe'], done); + }); + it('does not match +', function(done) { + var collection = new Whisper.ConversationCollection(); + return collection + .search('+') + .then(function() { + assert.isUndefined(collection.get(convo.id), 'got result for "+"'); + done(); + }) + .catch(done); + }); + }); +})(); diff --git a/test/models/messages_test.js b/test/models/messages_test.js index 996ef7229..87d4f1f67 100644 --- a/test/models/messages_test.js +++ b/test/models/messages_test.js @@ -1,188 +1,219 @@ /* * vim: ts=4:sw=4:expandtab */ -(function () { - 'use strict'; - - function deleteAllMessages() { - return new Promise(function(resolve, reject) { - var messages = new Whisper.MessageCollection(); - return messages.fetch().then(function() { - messages.destroyAll(); - resolve(); - }, reject); - }); - } - - var attributes = { type: 'outgoing', - body: 'hi', - conversationId: 'foo', - attachments: [], - received_at: new Date().getTime() }; - - var attachment = { data: 'datasaurus', - contentType: 'plain/text' }; - - var source = '+14155555555'; - - describe('MessageCollection', function() { - before(function() { - return Promise.all([ - deleteAllMessages(), - ConversationController.load() - ]); - }); - after(function() { - return deleteAllMessages(); - }); - - it('has no image url', function() { - var messages = new Whisper.MessageCollection(); - var message = messages.add(attributes); - assert.isNull(message.getImageUrl()); - }); - - it('updates image url', function() { - var messages = new Whisper.MessageCollection(); - var message = messages.add({ attachments: [attachment] }); - - var firstUrl = message.getImageUrl(); - message.updateImageUrl(); - var secondUrl = message.getImageUrl(); - assert.notEqual(secondUrl, firstUrl); - }); - - it('gets outgoing contact', function() { - var messages = new Whisper.MessageCollection(); - var message = messages.add(attributes); - message.getContact(); - }); - - it('gets incoming contact', function() { - var messages = new Whisper.MessageCollection(); - var message = messages.add({ - type: 'incoming', - source: source - }); - message.getContact(); - }); - - it('adds without saving', function() { - var messages = new Whisper.MessageCollection(); - var message = messages.add(attributes); - assert.notEqual(messages.length, 0); - - var messages = new Whisper.MessageCollection(); - assert.strictEqual(messages.length, 0); - }); - - it('saves asynchronously', function(done) { - new Whisper.MessageCollection().add(attributes).save().then(done); - }); - - it('fetches persistent messages', function(done) { - var messages = new Whisper.MessageCollection(); - assert.strictEqual(messages.length, 0); - messages.fetch().then(function() { - assert.notEqual(messages.length, 0); - var m = messages.at(0).attributes; - _.each(attributes, function(val, key) { - assert.deepEqual(m[key], val); - }); - done(); - }); - }); - - it('destroys persistent messages', function(done) { - var messages = new Whisper.MessageCollection(); - messages.fetch().then(function() { - messages.destroyAll().then(function() { - var messages = new Whisper.MessageCollection(); - messages.fetch().then(function() { - assert.strictEqual(messages.length, 0); - done(); - }); - }); - }); - }); - - it('should be ordered oldest to newest', function() { - var messages = new Whisper.MessageCollection(); - // Timestamps - var today = new Date(); - var tomorrow = new Date(); - tomorrow.setDate(today.getDate()+1); - - // Add threads - messages.add({ received_at: today }); - messages.add({ received_at: tomorrow }); - - var models = messages.models; - var firstTimestamp = models[0].get('received_at').getTime(); - var secondTimestamp = models[1].get('received_at').getTime(); - - // Compare timestamps - assert(firstTimestamp < secondTimestamp); - }); - - it('checks if is incoming message', function() { - var messages = new Whisper.MessageCollection(); - var message = messages.add(attributes); - assert.notOk(message.isIncoming()); - message = messages.add({type: 'incoming'}); - assert.ok(message.isIncoming()); - }); - - it('checks if is outgoing message', function() { - var messages = new Whisper.MessageCollection(); - var message = messages.add(attributes); - assert.ok(message.isOutgoing()); - message = messages.add({type: 'incoming'}); - assert.notOk(message.isOutgoing()); - }); - - it('checks if is group update', function() { - var messages = new Whisper.MessageCollection(); - var message = messages.add(attributes); - assert.notOk(message.isGroupUpdate()); - - message = messages.add({group_update: true}); - assert.ok(message.isGroupUpdate()); - }); - - it('returns an accurate description', function() { - var messages = new Whisper.MessageCollection(); - var message = messages.add(attributes); - - assert.equal(message.getDescription(), 'hi', 'If no group updates or end session flags, return message body.'); - - message = messages.add({group_update: {left: 'Alice'}}); - assert.equal(message.getDescription(), 'Alice left the group.', 'Notes one person leaving the group.'); - - message = messages.add({group_update: {name: 'blerg'}}); - assert.equal(message.getDescription(), 'Updated the group. Title is now \'blerg\'.', 'Returns a single notice if only group_updates.name changes.'); - - message = messages.add({group_update: {joined: ['Bob']}}); - assert.equal(message.getDescription(), 'Updated the group. Bob joined the group.', 'Returns a single notice if only group_updates.joined changes.'); - - message = messages.add({group_update: {joined: ['Bob', 'Alice', 'Eve']}}); - assert.equal(message.getDescription(), 'Updated the group. Bob, Alice, Eve joined the group.', 'Notes when >1 person joins the group.'); - - message = messages.add({group_update: {joined: ['Bob'], name: 'blerg'}}); - assert.equal(message.getDescription(), 'Updated the group. Title is now \'blerg\'. Bob joined the group.', 'Notes when there are multiple changes to group_updates properties.'); - - message = messages.add({flags: true}); - assert.equal(message.getDescription(), i18n('sessionEnded')); - }); - - it('checks if it is end of the session', function() { - var messages = new Whisper.MessageCollection(); - var message = messages.add(attributes); - assert.notOk(message.isEndSession()); - - message = messages.add({flags: true}); - assert.ok(message.isEndSession()); - }); +(function() { + 'use strict'; + function deleteAllMessages() { + return new Promise(function(resolve, reject) { + var messages = new Whisper.MessageCollection(); + return messages.fetch().then(function() { + messages.destroyAll(); + resolve(); + }, reject); }); + } + + var attributes = { + type: 'outgoing', + body: 'hi', + conversationId: 'foo', + attachments: [], + received_at: new Date().getTime(), + }; + + var attachment = { + data: 'datasaurus', + contentType: 'plain/text', + }; + + var source = '+14155555555'; + + describe('MessageCollection', function() { + before(function() { + return Promise.all([deleteAllMessages(), ConversationController.load()]); + }); + after(function() { + return deleteAllMessages(); + }); + + it('has no image url', function() { + var messages = new Whisper.MessageCollection(); + var message = messages.add(attributes); + assert.isNull(message.getImageUrl()); + }); + + it('updates image url', function() { + var messages = new Whisper.MessageCollection(); + var message = messages.add({ attachments: [attachment] }); + + var firstUrl = message.getImageUrl(); + message.updateImageUrl(); + var secondUrl = message.getImageUrl(); + assert.notEqual(secondUrl, firstUrl); + }); + + it('gets outgoing contact', function() { + var messages = new Whisper.MessageCollection(); + var message = messages.add(attributes); + message.getContact(); + }); + + it('gets incoming contact', function() { + var messages = new Whisper.MessageCollection(); + var message = messages.add({ + type: 'incoming', + source: source, + }); + message.getContact(); + }); + + it('adds without saving', function() { + var messages = new Whisper.MessageCollection(); + var message = messages.add(attributes); + assert.notEqual(messages.length, 0); + + var messages = new Whisper.MessageCollection(); + assert.strictEqual(messages.length, 0); + }); + + it('saves asynchronously', function(done) { + new Whisper.MessageCollection() + .add(attributes) + .save() + .then(done); + }); + + it('fetches persistent messages', function(done) { + var messages = new Whisper.MessageCollection(); + assert.strictEqual(messages.length, 0); + messages.fetch().then(function() { + assert.notEqual(messages.length, 0); + var m = messages.at(0).attributes; + _.each(attributes, function(val, key) { + assert.deepEqual(m[key], val); + }); + done(); + }); + }); + + it('destroys persistent messages', function(done) { + var messages = new Whisper.MessageCollection(); + messages.fetch().then(function() { + messages.destroyAll().then(function() { + var messages = new Whisper.MessageCollection(); + messages.fetch().then(function() { + assert.strictEqual(messages.length, 0); + done(); + }); + }); + }); + }); + + it('should be ordered oldest to newest', function() { + var messages = new Whisper.MessageCollection(); + // Timestamps + var today = new Date(); + var tomorrow = new Date(); + tomorrow.setDate(today.getDate() + 1); + + // Add threads + messages.add({ received_at: today }); + messages.add({ received_at: tomorrow }); + + var models = messages.models; + var firstTimestamp = models[0].get('received_at').getTime(); + var secondTimestamp = models[1].get('received_at').getTime(); + + // Compare timestamps + assert(firstTimestamp < secondTimestamp); + }); + + it('checks if is incoming message', function() { + var messages = new Whisper.MessageCollection(); + var message = messages.add(attributes); + assert.notOk(message.isIncoming()); + message = messages.add({ type: 'incoming' }); + assert.ok(message.isIncoming()); + }); + + it('checks if is outgoing message', function() { + var messages = new Whisper.MessageCollection(); + var message = messages.add(attributes); + assert.ok(message.isOutgoing()); + message = messages.add({ type: 'incoming' }); + assert.notOk(message.isOutgoing()); + }); + + it('checks if is group update', function() { + var messages = new Whisper.MessageCollection(); + var message = messages.add(attributes); + assert.notOk(message.isGroupUpdate()); + + message = messages.add({ group_update: true }); + assert.ok(message.isGroupUpdate()); + }); + + it('returns an accurate description', function() { + var messages = new Whisper.MessageCollection(); + var message = messages.add(attributes); + + assert.equal( + message.getDescription(), + 'hi', + 'If no group updates or end session flags, return message body.' + ); + + message = messages.add({ group_update: { left: 'Alice' } }); + assert.equal( + message.getDescription(), + 'Alice left the group.', + 'Notes one person leaving the group.' + ); + + message = messages.add({ group_update: { name: 'blerg' } }); + assert.equal( + message.getDescription(), + "Updated the group. Title is now 'blerg'.", + 'Returns a single notice if only group_updates.name changes.' + ); + + message = messages.add({ group_update: { joined: ['Bob'] } }); + assert.equal( + message.getDescription(), + 'Updated the group. Bob joined the group.', + 'Returns a single notice if only group_updates.joined changes.' + ); + + message = messages.add({ + group_update: { joined: ['Bob', 'Alice', 'Eve'] }, + }); + assert.equal( + message.getDescription(), + 'Updated the group. Bob, Alice, Eve joined the group.', + 'Notes when >1 person joins the group.' + ); + + message = messages.add({ + group_update: { joined: ['Bob'], name: 'blerg' }, + }); + assert.equal( + message.getDescription(), + "Updated the group. Title is now 'blerg'. Bob joined the group.", + 'Notes when there are multiple changes to group_updates properties.' + ); + + message = messages.add({ flags: true }); + assert.equal(message.getDescription(), i18n('sessionEnded')); + }); + + it('checks if it is end of the session', function() { + var messages = new Whisper.MessageCollection(); + var message = messages.add(attributes); + assert.notOk(message.isEndSession()); + + message = messages.add({ flags: true }); + assert.ok(message.isEndSession()); + }); + }); })(); diff --git a/test/modules/.eslintrc.js b/test/modules/.eslintrc.js index 1051e858e..eb9d189ca 100644 --- a/test/modules/.eslintrc.js +++ b/test/modules/.eslintrc.js @@ -6,7 +6,7 @@ module.exports = { browser: true, }, - "globals": { + globals: { check: true, gen: true, }, @@ -17,11 +17,14 @@ module.exports = { rules: { // We still get the value of this rule, it just allows for dev deps - 'import/no-extraneous-dependencies': ['error', { - devDependencies: true - }], + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, + }, + ], // We want to keep each test structured the same, even if its contents are tiny 'arrow-body-style': 'off', - } + }, }; diff --git a/test/modules/debuglogs_test.js b/test/modules/debuglogs_test.js index 8d5552240..dfa1e83f2 100644 --- a/test/modules/debuglogs_test.js +++ b/test/modules/debuglogs_test.js @@ -3,11 +3,12 @@ const got = require('got'); const debuglogs = require('../../js/modules/debuglogs'); - describe('debuglogs', () => { describe('upload', () => { it('should upload log content', async () => { - const nonce = Math.random().toString().slice(2); + const nonce = Math.random() + .toString() + .slice(2); const url = await debuglogs.upload(nonce); const { body } = await got.get(url); diff --git a/test/modules/privacy_test.js b/test/modules/privacy_test.js index 3131bdde5..f9f4f71d9 100644 --- a/test/modules/privacy_test.js +++ b/test/modules/privacy_test.js @@ -4,17 +4,18 @@ const { assert } = require('chai'); const Privacy = require('../../js/modules/privacy'); - const APP_ROOT_PATH = path.join(__dirname, '..', '..', '..'); describe('Privacy', () => { describe('redactPhoneNumbers', () => { it('should redact all phone numbers', () => { - const text = 'This is a log line with a phone number +12223334455\n' + + const text = + 'This is a log line with a phone number +12223334455\n' + 'and another one +13334445566'; const actual = Privacy.redactPhoneNumbers(text); - const expected = 'This is a log line with a phone number +[REDACTED]455\n' + + const expected = + 'This is a log line with a phone number +[REDACTED]455\n' + 'and another one +[REDACTED]566'; assert.equal(actual, expected); }); @@ -22,11 +23,13 @@ describe('Privacy', () => { describe('redactGroupIds', () => { it('should redact all group IDs', () => { - const text = 'This is a log line with two group IDs: group(123456789)\n' + + const text = + 'This is a log line with two group IDs: group(123456789)\n' + 'and group(abcdefghij)'; const actual = Privacy.redactGroupIds(text); - const expected = 'This is a log line with two group IDs: group([REDACTED]789)\n' + + const expected = + 'This is a log line with two group IDs: group([REDACTED]789)\n' + 'and group([REDACTED]hij)'; assert.equal(actual, expected); }); @@ -35,7 +38,8 @@ describe('Privacy', () => { describe('redactAll', () => { it('should redact all sensitive information', () => { const encodedAppRootPath = APP_ROOT_PATH.replace(/ /g, '%20'); - const text = 'This is a log line with sensitive information:\n' + + const text = + 'This is a log line with sensitive information:\n' + `path1 ${APP_ROOT_PATH}/main.js\n` + 'phone1 +12223334455 ipsum\n' + 'group1 group(123456789) doloret\n' + @@ -44,7 +48,8 @@ describe('Privacy', () => { 'group2 group(abcdefghij) doloret\n'; const actual = Privacy.redactAll(text); - const expected = 'This is a log line with sensitive information:\n' + + const expected = + 'This is a log line with sensitive information:\n' + 'path1 [REDACTED]/main.js\n' + 'phone1 +[REDACTED]455 ipsum\n' + 'group1 group([REDACTED]789) doloret\n' + @@ -58,12 +63,14 @@ describe('Privacy', () => { describe('_redactPath', () => { it('should redact file paths', () => { const testPath = '/Users/meow/Library/Application Support/Signal Beta'; - const text = 'This is a log line with sensitive information:\n' + + const text = + 'This is a log line with sensitive information:\n' + `path1 ${testPath}/main.js\n` + 'phone1 +12223334455 ipsum\n'; const actual = Privacy._redactPath(testPath)(text); - const expected = 'This is a log line with sensitive information:\n' + + const expected = + 'This is a log line with sensitive information:\n' + 'path1 [REDACTED]/main.js\n' + 'phone1 +12223334455 ipsum\n'; assert.equal(actual, expected); @@ -72,14 +79,16 @@ describe('Privacy', () => { it('should redact URL-encoded paths', () => { const testPath = '/Users/meow/Library/Application Support/Signal Beta'; const encodedTestPath = encodeURI(testPath); - const text = 'This is a log line with sensitive information:\n' + + const text = + 'This is a log line with sensitive information:\n' + `path1 ${testPath}/main.js\n` + 'phone1 +12223334455 ipsum\n' + 'group1 group(123456789) doloret\n' + `path2 file:///${encodedTestPath}/js/background.js.`; const actual = Privacy._redactPath(testPath)(text); - const expected = 'This is a log line with sensitive information:\n' + + const expected = + 'This is a log line with sensitive information:\n' + 'path1 [REDACTED]/main.js\n' + 'phone1 +12223334455 ipsum\n' + 'group1 group(123456789) doloret\n' + @@ -88,17 +97,20 @@ describe('Privacy', () => { }); it('should redact stack traces with both forward and backslashes', () => { - const testPath = 'C:/Users/Meow/AppData/Local/Programs/signal-desktop-beta'; + const testPath = + 'C:/Users/Meow/AppData/Local/Programs/signal-desktop-beta'; const modifiedTestPath = 'C:\\Users\\Meow\\AppData\\Local\\Programs\\signal-desktop-beta'; - const text = 'This is a log line with sensitive information:\n' + + const text = + 'This is a log line with sensitive information:\n' + `path1 ${testPath}\\main.js\n` + 'phone1 +12223334455 ipsum\n' + 'group1 group(123456789) doloret\n' + `path2 ${modifiedTestPath}\\js\\background.js.`; const actual = Privacy._redactPath(testPath)(text); - const expected = 'This is a log line with sensitive information:\n' + + const expected = + 'This is a log line with sensitive information:\n' + 'path1 [REDACTED]\\main.js\n' + 'phone1 +12223334455 ipsum\n' + 'group1 group(123456789) doloret\n' + @@ -107,17 +119,20 @@ describe('Privacy', () => { }); it('should redact stack traces with escaped backslashes', () => { - const testPath = 'C:\\Users\\Meow\\AppData\\Local\\Programs\\signal-desktop-beta'; + const testPath = + 'C:\\Users\\Meow\\AppData\\Local\\Programs\\signal-desktop-beta'; const modifiedTestPath = 'C:\\\\Users\\\\Meow\\\\AppData\\\\Local\\\\Programs\\\\signal-desktop-beta'; - const text = 'This is a log line with sensitive information:\n' + + const text = + 'This is a log line with sensitive information:\n' + `path1 ${testPath}\\main.js\n` + 'phone1 +12223334455 ipsum\n' + 'group1 group(123456789) doloret\n' + `path2 ${modifiedTestPath}\\js\\background.js.`; const actual = Privacy._redactPath(testPath)(text); - const expected = 'This is a log line with sensitive information:\n' + + const expected = + 'This is a log line with sensitive information:\n' + 'path1 [REDACTED]\\main.js\n' + 'phone1 +12223334455 ipsum\n' + 'group1 group(123456789) doloret\n' + diff --git a/test/modules/startup_test.js b/test/modules/startup_test.js index d39aab3c0..8f79516bd 100644 --- a/test/modules/startup_test.js +++ b/test/modules/startup_test.js @@ -3,7 +3,6 @@ const { assert } = require('chai'); const Startup = require('../../js/modules/startup'); - describe('Startup', () => { const sandbox = sinon.createSandbox(); diff --git a/test/modules/types/attachment_test.js b/test/modules/types/attachment_test.js index fe930a999..7cc131a65 100644 --- a/test/modules/types/attachment_test.js +++ b/test/modules/types/attachment_test.js @@ -3,7 +3,9 @@ require('mocha-testcheck').install(); const { assert } = require('chai'); const Attachment = require('../../../js/modules/types/attachment'); -const { stringToArrayBuffer } = require('../../../js/modules/string_to_array_buffer'); +const { + stringToArrayBuffer, +} = require('../../../js/modules/string_to_array_buffer'); describe('Attachment', () => { describe('replaceUnicodeOrderOverrides', () => { @@ -67,7 +69,7 @@ describe('Attachment', () => { check.it( 'should ignore non-order-override characters', gen.string.suchThat(hasNoUnicodeOrderOverrides), - (fileName) => { + fileName => { const input = { contentType: 'image/jpeg', data: null, @@ -120,15 +122,14 @@ describe('Attachment', () => { }; const expectedAttachmentData = stringToArrayBuffer('Above us only sky'); - const writeNewAttachmentData = async (attachmentData) => { + const writeNewAttachmentData = async attachmentData => { assert.deepEqual(attachmentData, expectedAttachmentData); return 'abc/abcdefgh123456789'; }; - const actual = await Attachment.migrateDataToFileSystem( - input, - { writeNewAttachmentData } - ); + const actual = await Attachment.migrateDataToFileSystem(input, { + writeNewAttachmentData, + }); assert.deepEqual(actual, expected); }); @@ -145,13 +146,11 @@ describe('Attachment', () => { size: 1111, }; - const writeNewAttachmentData = async () => - 'abc/abcdefgh123456789'; + const writeNewAttachmentData = async () => 'abc/abcdefgh123456789'; - const actual = await Attachment.migrateDataToFileSystem( - input, - { writeNewAttachmentData } - ); + const actual = await Attachment.migrateDataToFileSystem(input, { + writeNewAttachmentData, + }); assert.deepEqual(actual, expected); }); @@ -163,11 +162,12 @@ describe('Attachment', () => { size: 1111, }; - const writeNewAttachmentData = async () => - 'abc/abcdefgh123456789'; + const writeNewAttachmentData = async () => 'abc/abcdefgh123456789'; try { - await Attachment.migrateDataToFileSystem(input, { writeNewAttachmentData }); + await Attachment.migrateDataToFileSystem(input, { + writeNewAttachmentData, + }); } catch (error) { assert.strictEqual( error.message, diff --git a/test/modules/types/errors_test.js b/test/modules/types/errors_test.js index 9cca14062..5d988492a 100644 --- a/test/modules/types/errors_test.js +++ b/test/modules/types/errors_test.js @@ -4,7 +4,6 @@ const { assert } = require('chai'); const Errors = require('../../../js/modules/types/errors'); - const APP_ROOT_PATH = Path.join(__dirname, '..', '..', '..'); describe('Errors', () => { @@ -15,7 +14,11 @@ describe('Errors', () => { const formattedError = Errors.toLogFormat(error); assert.include(formattedError, 'errors_test.js'); - assert.include(formattedError, APP_ROOT_PATH, 'Formatted stack has app path'); + assert.include( + formattedError, + APP_ROOT_PATH, + 'Formatted stack has app path' + ); }); it('should return error string representation if stack is missing', () => { @@ -28,12 +31,7 @@ describe('Errors', () => { assert.strictEqual(formattedError, 'Error: boom'); }); - [ - 0, - false, - null, - undefined, - ].forEach((value) => { + [0, false, null, undefined].forEach(value => { it(`should return \`${value}\` argument`, () => { const formattedNonError = Errors.toLogFormat(value); assert.strictEqual(formattedNonError, value); diff --git a/test/modules/types/message_test.js b/test/modules/types/message_test.js index 15f285813..33516b661 100644 --- a/test/modules/types/message_test.js +++ b/test/modules/types/message_test.js @@ -2,8 +2,9 @@ const { assert } = require('chai'); const sinon = require('sinon'); const Message = require('../../../js/modules/types/message'); -const { stringToArrayBuffer } = require('../../../js/modules/string_to_array_buffer'); - +const { + stringToArrayBuffer, +} = require('../../../js/modules/string_to_array_buffer'); describe('Message', () => { describe('createAttachmentDataWriter', () => { @@ -18,8 +19,9 @@ describe('Message', () => { }; const writeExistingAttachmentData = () => {}; - const actual = - await Message.createAttachmentDataWriter(writeExistingAttachmentData)(input); + const actual = await Message.createAttachmentDataWriter( + writeExistingAttachmentData + )(input); assert.deepEqual(actual, expected); }); @@ -36,8 +38,9 @@ describe('Message', () => { }; const writeExistingAttachmentData = () => {}; - const actual = - await Message.createAttachmentDataWriter(writeExistingAttachmentData)(input); + const actual = await Message.createAttachmentDataWriter( + writeExistingAttachmentData + )(input); assert.deepEqual(actual, expected); }); @@ -45,26 +48,34 @@ describe('Message', () => { const input = { body: 'Imagine there is no heaven…', schemaVersion: 4, - attachments: [{ - path: 'ab/abcdefghi', - data: stringToArrayBuffer('It’s easy if you try'), - }], + attachments: [ + { + path: 'ab/abcdefghi', + data: stringToArrayBuffer('It’s easy if you try'), + }, + ], }; const expected = { body: 'Imagine there is no heaven…', schemaVersion: 4, - attachments: [{ - path: 'ab/abcdefghi', - }], + attachments: [ + { + path: 'ab/abcdefghi', + }, + ], }; - const writeExistingAttachmentData = (attachment) => { + const writeExistingAttachmentData = attachment => { assert.equal(attachment.path, 'ab/abcdefghi'); - assert.deepEqual(attachment.data, stringToArrayBuffer('It’s easy if you try')); + assert.deepEqual( + attachment.data, + stringToArrayBuffer('It’s easy if you try') + ); }; - const actual = - await Message.createAttachmentDataWriter(writeExistingAttachmentData)(input); + const actual = await Message.createAttachmentDataWriter( + writeExistingAttachmentData + )(input); assert.deepEqual(actual, expected); }); @@ -74,12 +85,14 @@ describe('Message', () => { schemaVersion: 4, attachments: [], quote: { - attachments: [{ - thumbnail: { - path: 'ab/abcdefghi', - data: stringToArrayBuffer('It’s easy if you try'), + attachments: [ + { + thumbnail: { + path: 'ab/abcdefghi', + data: stringToArrayBuffer('It’s easy if you try'), + }, }, - }], + ], }, }; const expected = { @@ -87,21 +100,27 @@ describe('Message', () => { schemaVersion: 4, attachments: [], quote: { - attachments: [{ - thumbnail: { - path: 'ab/abcdefghi', + attachments: [ + { + thumbnail: { + path: 'ab/abcdefghi', + }, }, - }], + ], }, }; - const writeExistingAttachmentData = (attachment) => { + const writeExistingAttachmentData = attachment => { assert.equal(attachment.path, 'ab/abcdefghi'); - assert.deepEqual(attachment.data, stringToArrayBuffer('It’s easy if you try')); + assert.deepEqual( + attachment.data, + stringToArrayBuffer('It’s easy if you try') + ); }; - const actual = - await Message.createAttachmentDataWriter(writeExistingAttachmentData)(input); + const actual = await Message.createAttachmentDataWriter( + writeExistingAttachmentData + )(input); assert.deepEqual(actual, expected); }); }); @@ -142,18 +161,22 @@ describe('Message', () => { it('should inherit existing attachment schema version', () => { const input = { body: 'Imagine there is no heaven…', - attachments: [{ - contentType: 'image/jpeg', - fileName: 'lennon.jpg', - schemaVersion: 7, - }], + attachments: [ + { + contentType: 'image/jpeg', + fileName: 'lennon.jpg', + schemaVersion: 7, + }, + ], }; const expected = { body: 'Imagine there is no heaven…', - attachments: [{ - contentType: 'image/jpeg', - fileName: 'lennon.jpg', - }], + attachments: [ + { + contentType: 'image/jpeg', + fileName: 'lennon.jpg', + }, + ], schemaVersion: 7, }; @@ -166,30 +189,36 @@ describe('Message', () => { describe('upgradeSchema', () => { it('should upgrade an unversioned message to the latest version', async () => { const input = { - attachments: [{ - contentType: 'application/json', - data: stringToArrayBuffer('It’s easy if you try'), - fileName: 'test\u202Dfig.exe', - size: 1111, - }], + attachments: [ + { + contentType: 'application/json', + data: stringToArrayBuffer('It’s easy if you try'), + fileName: 'test\u202Dfig.exe', + size: 1111, + }, + ], schemaVersion: 0, }; const expected = { - attachments: [{ - contentType: 'application/json', - path: 'abc/abcdefg', - fileName: 'test\uFFFDfig.exe', - size: 1111, - }], + attachments: [ + { + contentType: 'application/json', + path: 'abc/abcdefg', + fileName: 'test\uFFFDfig.exe', + size: 1111, + }, + ], hasAttachments: 1, hasVisualMediaAttachments: undefined, hasFileAttachments: 1, schemaVersion: Message.CURRENT_SCHEMA_VERSION, }; - const expectedAttachmentData = stringToArrayBuffer('It’s easy if you try'); + const expectedAttachmentData = stringToArrayBuffer( + 'It’s easy if you try' + ); const context = { - writeNewAttachmentData: async (attachmentData) => { + writeNewAttachmentData: async attachmentData => { assert.deepEqual(attachmentData, expectedAttachmentData); return 'abc/abcdefg'; }, @@ -201,21 +230,25 @@ describe('Message', () => { context('with multiple upgrade steps', () => { it('should return last valid message when any upgrade step fails', async () => { const input = { - attachments: [{ - contentType: 'application/json', - data: null, - fileName: 'test\u202Dfig.exe', - size: 1111, - }], + attachments: [ + { + contentType: 'application/json', + data: null, + fileName: 'test\u202Dfig.exe', + size: 1111, + }, + ], schemaVersion: 0, }; const expected = { - attachments: [{ - contentType: 'application/json', - data: null, - fileName: 'test\u202Dfig.exe', - size: 1111, - }], + attachments: [ + { + contentType: 'application/json', + data: null, + fileName: 'test\u202Dfig.exe', + size: 1111, + }, + ], hasUpgradedToVersion1: true, schemaVersion: 1, }; @@ -241,21 +274,25 @@ describe('Message', () => { it('should skip out-of-order upgrade steps', async () => { const input = { - attachments: [{ - contentType: 'application/json', - data: null, - fileName: 'test\u202Dfig.exe', - size: 1111, - }], + attachments: [ + { + contentType: 'application/json', + data: null, + fileName: 'test\u202Dfig.exe', + size: 1111, + }, + ], schemaVersion: 0, }; const expected = { - attachments: [{ - contentType: 'application/json', - data: null, - fileName: 'test\u202Dfig.exe', - size: 1111, - }], + attachments: [ + { + contentType: 'application/json', + data: null, + fileName: 'test\u202Dfig.exe', + size: 1111, + }, + ], schemaVersion: 2, hasUpgradedToVersion1: true, hasUpgradedToVersion2: true, @@ -352,7 +389,9 @@ describe('Message', () => { describe('_mapQuotedAttachments', () => { it('handles message with no quote', async () => { - const upgradeAttachment = sinon.stub().throws(new Error("Shouldn't be called")); + const upgradeAttachment = sinon + .stub() + .throws(new Error("Shouldn't be called")); const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); const message = { @@ -363,7 +402,9 @@ describe('Message', () => { }); it('handles quote with no attachments', async () => { - const upgradeAttachment = sinon.stub().throws(new Error("Shouldn't be called")); + const upgradeAttachment = sinon + .stub() + .throws(new Error("Shouldn't be called")); const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); const message = { @@ -384,7 +425,9 @@ describe('Message', () => { }); it('handles zero attachments', async () => { - const upgradeAttachment = sinon.stub().throws(new Error("Shouldn't be called")); + const upgradeAttachment = sinon + .stub() + .throws(new Error("Shouldn't be called")); const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); const message = { @@ -399,7 +442,9 @@ describe('Message', () => { }); it('handles attachments with no thumbnail', async () => { - const upgradeAttachment = sinon.stub().throws(new Error("Shouldn't be called")); + const upgradeAttachment = sinon + .stub() + .throws(new Error("Shouldn't be called")); const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); const message = { @@ -414,30 +459,36 @@ describe('Message', () => { }); it('eliminates thumbnails with no data fielkd', async () => { - const upgradeAttachment = sinon.stub().throws(new Error("Shouldn't be called")); + const upgradeAttachment = sinon + .stub() + .throws(new Error("Shouldn't be called")); const upgradeVersion = Message._mapQuotedAttachments(upgradeAttachment); const message = { body: 'hey there!', quote: { text: 'hey!', - attachments: [{ - fileName: 'cat.gif', - contentType: 'image/gif', - thumbnail: { - fileName: 'failed to download!', + attachments: [ + { + fileName: 'cat.gif', + contentType: 'image/gif', + thumbnail: { + fileName: 'failed to download!', + }, }, - }], + ], }, }; const expected = { body: 'hey there!', quote: { text: 'hey!', - attachments: [{ - contentType: 'image/gif', - fileName: 'cat.gif', - }], + attachments: [ + { + contentType: 'image/gif', + fileName: 'cat.gif', + }, + ], }, }; const result = await upgradeVersion(message); @@ -454,22 +505,26 @@ describe('Message', () => { body: 'hey there!', quote: { text: 'hey!', - attachments: [{ - thumbnail: { - data: 'data is here', + attachments: [ + { + thumbnail: { + data: 'data is here', + }, }, - }], + ], }, }; const expected = { body: 'hey there!', quote: { text: 'hey!', - attachments: [{ - thumbnail: { - path: '/new/path/on/disk', + attachments: [ + { + thumbnail: { + path: '/new/path/on/disk', + }, }, - }], + ], }, }; const result = await upgradeVersion(message); diff --git a/test/modules/types/mime_test.js b/test/modules/types/mime_test.js index ab62bc343..bc85780cd 100644 --- a/test/modules/types/mime_test.js +++ b/test/modules/types/mime_test.js @@ -2,7 +2,6 @@ const { assert } = require('chai'); const MIME = require('../../../ts/types/MIME'); - describe('MIME', () => { describe('isJPEG', () => { it('should return true for `image/jpeg`', () => { @@ -20,11 +19,10 @@ describe('MIME', () => { false, null, undefined, - ] - .forEach((value) => { - it(`should return false for \`${value}\``, () => { - assert.isFalse(MIME.isJPEG(value)); - }); + ].forEach(value => { + it(`should return false for \`${value}\``, () => { + assert.isFalse(MIME.isJPEG(value)); }); + }); }); }); diff --git a/test/modules/types/schema_version_test.js b/test/modules/types/schema_version_test.js index a3205c485..34a97d7a9 100644 --- a/test/modules/types/schema_version_test.js +++ b/test/modules/types/schema_version_test.js @@ -3,21 +3,16 @@ const { assert } = require('chai'); const SchemaVersion = require('../../../js/modules/types/schema_version'); - describe('SchemaVersion', () => { describe('isValid', () => { - check.it( - 'should return true for positive integers', - gen.posInt, - (input) => { - assert.isTrue(SchemaVersion.isValid(input)); - } - ); + check.it('should return true for positive integers', gen.posInt, input => { + assert.isTrue(SchemaVersion.isValid(input)); + }); check.it( 'should return false for any other value', gen.primitive.suchThat(value => typeof value !== 'number' || value < 0), - (input) => { + input => { assert.isFalse(SchemaVersion.isValid(input)); } ); diff --git a/test/modules/types/settings_test.js b/test/modules/types/settings_test.js index 1ae99996b..59f195348 100644 --- a/test/modules/types/settings_test.js +++ b/test/modules/types/settings_test.js @@ -3,7 +3,6 @@ const { assert } = require('chai'); const Settings = require('../../../js/modules/types/settings'); - describe('Settings', () => { const sandbox = sinon.createSandbox(); diff --git a/test/reliable_trigger_test.js b/test/reliable_trigger_test.js index 56f84351d..469346f95 100644 --- a/test/reliable_trigger_test.js +++ b/test/reliable_trigger_test.js @@ -1,147 +1,151 @@ 'use strict'; describe('ReliableTrigger', function() { - describe('trigger', function() { - var Model, model; + describe('trigger', function() { + var Model, model; - before(function() { - Model = Backbone.Model; - }); - - beforeEach(function() { - model = new Model(); - }); - - it('returns successfully if this._events is falsey', function() { - model._events = null; - model.trigger('click'); - }); - it('handles map of events to trigger', function() { - var a = 0, b = 0; - model.on('a', function(arg) { - a = arg; - }); - model.on('b', function(arg) { - b = arg; - }); - - model.trigger({ - a: 1, - b: 2 - }); - - assert.strictEqual(a, 1); - assert.strictEqual(b, 2); - }); - it('handles space-separated list of events to trigger', function() { - var a = false, b = false; - model.on('a', function() { - a = true; - }); - model.on('b', function() { - b = true; - }); - - model.trigger('a b'); - - assert.strictEqual(a, true); - assert.strictEqual(b, true); - }); - it('calls all clients registered for "all" event', function() { - var count = 0; - model.on('all', function() { - count += 1; - }); - - model.trigger('left'); - model.trigger('right'); - - assert.strictEqual(count, 2); - }); - it('calls all clients registered for target event', function() { - var a = false, b = false; - model.on('event', function() { - a = true; - }); - model.on('event', function() { - b = true; - }); - - model.trigger('event'); - - assert.strictEqual(a, true); - assert.strictEqual(b, true); - }); - it('successfully returns and calls all clients even if first failed', function() { - var a = false, b = false; - model.on('event', function() { - a = true; - throw new Error('a is set, but exception is thrown'); - }); - model.on('event', function() { - b = true; - }); - - model.trigger('event'); - - assert.strictEqual(a, true); - assert.strictEqual(b, true); - }); - it('calls clients with no args', function() { - var called = false; - model.on('event', function() { - called = true; - }); - - model.trigger('event'); - - assert.strictEqual(called, true); - }); - it('calls clients with 1 arg', function() { - var args; - model.on('event', function() { - args = arguments; - }); - - model.trigger('event', 1); - - assert.strictEqual(args[0], 1); - }); - it('calls clients with 2 args', function() { - var args; - model.on('event', function() { - args = arguments; - }); - - model.trigger('event', 1, 2); - - assert.strictEqual(args[0], 1); - assert.strictEqual(args[1], 2); - }); - it('calls clients with 3 args', function() { - var args; - model.on('event', function() { - args = arguments; - }); - - model.trigger('event', 1, 2, 3); - - assert.strictEqual(args[0], 1); - assert.strictEqual(args[1], 2); - assert.strictEqual(args[2], 3); - }); - it('calls clients with 4+ args', function() { - var args; - model.on('event', function() { - args = arguments; - }); - - model.trigger('event', 1, 2, 3, 4); - - assert.strictEqual(args[0], 1); - assert.strictEqual(args[1], 2); - assert.strictEqual(args[2], 3); - assert.strictEqual(args[3], 4); - }); + before(function() { + Model = Backbone.Model; }); + + beforeEach(function() { + model = new Model(); + }); + + it('returns successfully if this._events is falsey', function() { + model._events = null; + model.trigger('click'); + }); + it('handles map of events to trigger', function() { + var a = 0, + b = 0; + model.on('a', function(arg) { + a = arg; + }); + model.on('b', function(arg) { + b = arg; + }); + + model.trigger({ + a: 1, + b: 2, + }); + + assert.strictEqual(a, 1); + assert.strictEqual(b, 2); + }); + it('handles space-separated list of events to trigger', function() { + var a = false, + b = false; + model.on('a', function() { + a = true; + }); + model.on('b', function() { + b = true; + }); + + model.trigger('a b'); + + assert.strictEqual(a, true); + assert.strictEqual(b, true); + }); + it('calls all clients registered for "all" event', function() { + var count = 0; + model.on('all', function() { + count += 1; + }); + + model.trigger('left'); + model.trigger('right'); + + assert.strictEqual(count, 2); + }); + it('calls all clients registered for target event', function() { + var a = false, + b = false; + model.on('event', function() { + a = true; + }); + model.on('event', function() { + b = true; + }); + + model.trigger('event'); + + assert.strictEqual(a, true); + assert.strictEqual(b, true); + }); + it('successfully returns and calls all clients even if first failed', function() { + var a = false, + b = false; + model.on('event', function() { + a = true; + throw new Error('a is set, but exception is thrown'); + }); + model.on('event', function() { + b = true; + }); + + model.trigger('event'); + + assert.strictEqual(a, true); + assert.strictEqual(b, true); + }); + it('calls clients with no args', function() { + var called = false; + model.on('event', function() { + called = true; + }); + + model.trigger('event'); + + assert.strictEqual(called, true); + }); + it('calls clients with 1 arg', function() { + var args; + model.on('event', function() { + args = arguments; + }); + + model.trigger('event', 1); + + assert.strictEqual(args[0], 1); + }); + it('calls clients with 2 args', function() { + var args; + model.on('event', function() { + args = arguments; + }); + + model.trigger('event', 1, 2); + + assert.strictEqual(args[0], 1); + assert.strictEqual(args[1], 2); + }); + it('calls clients with 3 args', function() { + var args; + model.on('event', function() { + args = arguments; + }); + + model.trigger('event', 1, 2, 3); + + assert.strictEqual(args[0], 1); + assert.strictEqual(args[1], 2); + assert.strictEqual(args[2], 3); + }); + it('calls clients with 4+ args', function() { + var args; + model.on('event', function() { + args = arguments; + }); + + model.trigger('event', 1, 2, 3, 4); + + assert.strictEqual(args[0], 1); + assert.strictEqual(args[1], 2); + assert.strictEqual(args[2], 3); + assert.strictEqual(args[3], 4); + }); + }); }); diff --git a/test/storage_test.js b/test/storage_test.js index 2364eba42..6361b0e08 100644 --- a/test/storage_test.js +++ b/test/storage_test.js @@ -1,6 +1,6 @@ 'use strict'; -describe("SignalProtocolStore", function() { +describe('SignalProtocolStore', function() { var identifier = '+5558675309'; var store; var identityKey; @@ -30,52 +30,66 @@ describe("SignalProtocolStore", function() { describe('getLocalRegistrationId', function() { it('retrieves my registration id', function(done) { - store.getLocalRegistrationId().then(function(reg) { - assert.strictEqual(reg, 1337); - }).then(done, done); + store + .getLocalRegistrationId() + .then(function(reg) { + assert.strictEqual(reg, 1337); + }) + .then(done, done); }); }); describe('getIdentityKeyPair', function() { it('retrieves my identity key', function(done) { - store.getIdentityKeyPair().then(function(key) { - assertEqualArrayBuffers(key.pubKey, identityKey.pubKey); - assertEqualArrayBuffers(key.privKey, identityKey.privKey); - }).then(done,done); + store + .getIdentityKeyPair() + .then(function(key) { + assertEqualArrayBuffers(key.pubKey, identityKey.pubKey); + assertEqualArrayBuffers(key.privKey, identityKey.privKey); + }) + .then(done, done); }); }); - - var IdentityKeyRecord = Backbone.Model.extend({ database: Whisper.Database, - storeName: 'identityKeys' + storeName: 'identityKeys', }); describe('saveIdentity', function() { - var record = new IdentityKeyRecord({id: identifier}); + var record = new IdentityKeyRecord({ id: identifier }); var address = new libsignal.SignalProtocolAddress(identifier, 1); it('stores identity keys', function(done) { - store.saveIdentity(address.toString(), testKey.pubKey).then(function() { - return store.loadIdentityKey(identifier).then(function(key) { - assertEqualArrayBuffers(key, testKey.pubKey); - }); - }).then(done,done); + store + .saveIdentity(address.toString(), testKey.pubKey) + .then(function() { + return store.loadIdentityKey(identifier).then(function(key) { + assertEqualArrayBuffers(key, testKey.pubKey); + }); + }) + .then(done, done); }); it('allows key changes', function(done) { var newIdentity = libsignal.crypto.getRandomBytes(33); - store.saveIdentity(address.toString(), testKey.pubKey).then(function() { - store.saveIdentity(address.toString(), newIdentity).then(function() { - done(); - }); - }).catch(done); + store + .saveIdentity(address.toString(), testKey.pubKey) + .then(function() { + store.saveIdentity(address.toString(), newIdentity).then(function() { + done(); + }); + }) + .catch(done); }); describe('When there is no existing key (first use)', function() { before(function(done) { store.removeIdentityKey(identifier).then(function() { - store.saveIdentity(address.toString(), testKey.pubKey).then(function() { - record.fetch().then(function() { done(); }); - }); + store + .saveIdentity(address.toString(), testKey.pubKey) + .then(function() { + record.fetch().then(function() { + done(); + }); + }); }); }); it('marks the key firstUse', function() { @@ -85,24 +99,33 @@ describe("SignalProtocolStore", function() { assert(record.get('timestamp')); }); it('sets the verified status to DEFAULT', function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.DEFAULT); + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.DEFAULT + ); }); }); describe('When there is a different existing key (non first use)', function() { var newIdentity = libsignal.crypto.getRandomBytes(33); var oldTimestamp = Date.now(); before(function(done) { - record.save({ - publicKey : testKey.pubKey, - firstUse : true, - timestamp : oldTimestamp, - nonblockingApproval : false, - verified : store.VerifiedStatus.DEFAULT - }).then(function() { - store.saveIdentity(address.toString(), newIdentity).then(function() { - record.fetch().then(function() { done(); }); + record + .save({ + publicKey: testKey.pubKey, + firstUse: true, + timestamp: oldTimestamp, + nonblockingApproval: false, + verified: store.VerifiedStatus.DEFAULT, + }) + .then(function() { + store + .saveIdentity(address.toString(), newIdentity) + .then(function() { + record.fetch().then(function() { + done(); + }); + }); }); - }); }); it('marks the key not firstUse', function() { assert(!record.get('firstUse')); @@ -113,102 +136,143 @@ describe("SignalProtocolStore", function() { describe('The previous verified status was DEFAULT', function() { before(function(done) { - record.save({ - publicKey : testKey.pubKey, - firstUse : true, - timestamp : oldTimestamp, - nonblockingApproval : false, - verified : store.VerifiedStatus.DEFAULT - }).then(function() { - store.saveIdentity(address.toString(), newIdentity).then(function() { - record.fetch().then(function() { done(); }); + record + .save({ + publicKey: testKey.pubKey, + firstUse: true, + timestamp: oldTimestamp, + nonblockingApproval: false, + verified: store.VerifiedStatus.DEFAULT, + }) + .then(function() { + store + .saveIdentity(address.toString(), newIdentity) + .then(function() { + record.fetch().then(function() { + done(); + }); + }); }); - }); }); it('sets the new key to unverified', function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.DEFAULT); + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.DEFAULT + ); }); }); describe('The previous verified status was VERIFIED', function() { before(function(done) { - record.save({ - publicKey : testKey.pubKey, - firstUse : true, - timestamp : oldTimestamp, - nonblockingApproval : false, - verified : store.VerifiedStatus.VERIFIED - }).then(function() { - store.saveIdentity(address.toString(), newIdentity).then(function() { - record.fetch().then(function() { done(); }); + record + .save({ + publicKey: testKey.pubKey, + firstUse: true, + timestamp: oldTimestamp, + nonblockingApproval: false, + verified: store.VerifiedStatus.VERIFIED, + }) + .then(function() { + store + .saveIdentity(address.toString(), newIdentity) + .then(function() { + record.fetch().then(function() { + done(); + }); + }); }); - }); }); it('sets the new key to unverified', function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.UNVERIFIED); + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.UNVERIFIED + ); }); }); describe('The previous verified status was UNVERIFIED', function() { before(function(done) { - record.save({ - publicKey : testKey.pubKey, - firstUse : true, - timestamp : oldTimestamp, - nonblockingApproval : false, - verified : store.VerifiedStatus.UNVERIFIED - }).then(function() { - store.saveIdentity(address.toString(), newIdentity).then(function() { - record.fetch().then(function() { done(); }); + record + .save({ + publicKey: testKey.pubKey, + firstUse: true, + timestamp: oldTimestamp, + nonblockingApproval: false, + verified: store.VerifiedStatus.UNVERIFIED, + }) + .then(function() { + store + .saveIdentity(address.toString(), newIdentity) + .then(function() { + record.fetch().then(function() { + done(); + }); + }); }); - }); }); it('sets the new key to unverified', function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.UNVERIFIED); + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.UNVERIFIED + ); }); }); }); describe('When the key has not changed', function() { var oldTimestamp = Date.now(); before(function(done) { - record.save({ - publicKey : testKey.pubKey, - timestamp : oldTimestamp, - nonblockingApproval : false, - verified : store.VerifiedStatus.DEFAULT - }).then(function() { done(); }); + record + .save({ + publicKey: testKey.pubKey, + timestamp: oldTimestamp, + nonblockingApproval: false, + verified: store.VerifiedStatus.DEFAULT, + }) + .then(function() { + done(); + }); }); describe('If it is marked firstUse', function() { before(function(done) { - record.save({ firstUse: true }).then(function() { done(); }); + record.save({ firstUse: true }).then(function() { + done(); + }); }); it('nothing changes', function(done) { - store.saveIdentity(address.toString(), testKey.pubKey, true).then(function() { - record.fetch().then(function() { - assert(!record.get('nonblockingApproval')); - assert.strictEqual(record.get('timestamp'), oldTimestamp); - done(); + store + .saveIdentity(address.toString(), testKey.pubKey, true) + .then(function() { + record.fetch().then(function() { + assert(!record.get('nonblockingApproval')); + assert.strictEqual(record.get('timestamp'), oldTimestamp); + done(); + }); }); - }); }); }); describe('If it is not marked firstUse', function() { before(function(done) { - record.save({ firstUse: false }).then(function() { done(); }); + record.save({ firstUse: false }).then(function() { + done(); + }); }); describe('If nonblocking approval is required', function() { var now; before(function(done) { now = Date.now(); - record.save({ timestamp: now }).then(function() { done(); }); + record.save({ timestamp: now }).then(function() { + done(); + }); }); it('sets non-blocking approval', function(done) { - store.saveIdentity(address.toString(), testKey.pubKey, true).then(function() { - record.fetch().then(function() { - assert.strictEqual(record.get('nonblockingApproval'), true); - assert.strictEqual(record.get('timestamp'), now); - assert.strictEqual(record.get('firstUse'), false); - done(); + store + .saveIdentity(address.toString(), testKey.pubKey, true) + .then(function() { + record.fetch().then(function() { + assert.strictEqual(record.get('nonblockingApproval'), true); + assert.strictEqual(record.get('timestamp'), now); + assert.strictEqual(record.get('firstUse'), false); + done(); + }); }); - }); }); }); }); @@ -221,40 +285,48 @@ describe("SignalProtocolStore", function() { before(function(done) { now = Date.now(); - record = new IdentityKeyRecord({id: identifier}); + record = new IdentityKeyRecord({ id: identifier }); validAttributes = { - publicKey : testKey.pubKey, - firstUse : true, - timestamp : now, - verified : store.VerifiedStatus.VERIFIED, - nonblockingApproval : false + publicKey: testKey.pubKey, + firstUse: true, + timestamp: now, + verified: store.VerifiedStatus.VERIFIED, + nonblockingApproval: false, }; - store.removeIdentityKey(identifier).then(function() { done(); }); + store.removeIdentityKey(identifier).then(function() { + done(); + }); }); describe('with valid attributes', function() { before(function(done) { - store.saveIdentityWithAttributes(identifier, validAttributes).then(function() { - return new Promise(function(resolve) { - record.fetch().then(resolve); - }); - }).then(done, done); + store + .saveIdentityWithAttributes(identifier, validAttributes) + .then(function() { + return new Promise(function(resolve) { + record.fetch().then(resolve); + }); + }) + .then(done, done); }); it('publicKey is saved', function() { - assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); + assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); }); it('firstUse is saved', function() { - assert.strictEqual(record.get('firstUse'), true); + assert.strictEqual(record.get('firstUse'), true); }); it('timestamp is saved', function() { - assert.strictEqual(record.get('timestamp'), now); + assert.strictEqual(record.get('timestamp'), now); }); it('verified is saved', function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.VERIFIED); + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.VERIFIED + ); }); it('nonblockingApproval is saved', function() { - assert.strictEqual(record.get('nonblockingApproval'), false); + assert.strictEqual(record.get('nonblockingApproval'), false); }); }); describe('with invalid attributes', function() { @@ -264,11 +336,14 @@ describe("SignalProtocolStore", function() { }); function testInvalidAttributes(done) { - store.saveIdentityWithAttributes(identifier, attributes).then(function() { - done(new Error("saveIdentityWithAttributes should have failed")); - }, function() { - done(); // good. we expect to fail with invalid attributes. - }); + store.saveIdentityWithAttributes(identifier, attributes).then( + function() { + done(new Error('saveIdentityWithAttributes should have failed')); + }, + function() { + done(); // good. we expect to fail with invalid attributes. + } + ); } it('rejects an invalid publicKey', function(done) { @@ -294,28 +369,32 @@ describe("SignalProtocolStore", function() { }); }); describe('setApproval', function() { - var record = new IdentityKeyRecord({id: identifier}); + var record = new IdentityKeyRecord({ id: identifier }); function fetchRecord() { return new Promise(function(resolve) { record.fetch().then(resolve); }); } - it ('sets nonblockingApproval', function(done) { - store.setApproval(identifier, true).then(fetchRecord).then(function() { - assert.strictEqual(record.get('nonblockingApproval'), true); - }).then(done, done); + it('sets nonblockingApproval', function(done) { + store + .setApproval(identifier, true) + .then(fetchRecord) + .then(function() { + assert.strictEqual(record.get('nonblockingApproval'), true); + }) + .then(done, done); }); }); describe('setVerified', function() { var record; function saveRecordDefault() { record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - firstUse : true, - timestamp : Date.now(), - verified : store.VerifiedStatus.DEFAULT, - nonblockingApproval : false + id: identifier, + publicKey: testKey.pubKey, + firstUse: true, + timestamp: Date.now(), + verified: store.VerifiedStatus.DEFAULT, + nonblockingApproval: false, }); return new Promise(function(resolve, reject) { record.save().then(resolve, reject); @@ -328,36 +407,52 @@ describe("SignalProtocolStore", function() { } describe('with no public key argument', function() { before(saveRecordDefault); - it ('updates the verified status', function() { - return store.setVerified( - identifier, store.VerifiedStatus.VERIFIED - ).then(fetchRecord).then(function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.VERIFIED); - assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); - }); + it('updates the verified status', function() { + return store + .setVerified(identifier, store.VerifiedStatus.VERIFIED) + .then(fetchRecord) + .then(function() { + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.VERIFIED + ); + assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); + }); }); }); describe('with the current public key', function() { before(saveRecordDefault); - it ('updates the verified status', function() { - return store.setVerified( - identifier, store.VerifiedStatus.VERIFIED, testKey.pubKey - ).then(fetchRecord).then(function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.VERIFIED); - assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); - }); + it('updates the verified status', function() { + return store + .setVerified( + identifier, + store.VerifiedStatus.VERIFIED, + testKey.pubKey + ) + .then(fetchRecord) + .then(function() { + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.VERIFIED + ); + assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); + }); }); }); describe('with a mismatching public key', function() { var newIdentity = libsignal.crypto.getRandomBytes(33); before(saveRecordDefault); - it ('does not change the record.', function() { - return store.setVerified( - identifier, store.VerifiedStatus.VERIFIED, newIdentity - ).then(fetchRecord).then(function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.DEFAULT); - assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); - }); + it('does not change the record.', function() { + return store + .setVerified(identifier, store.VerifiedStatus.VERIFIED, newIdentity) + .then(fetchRecord) + .then(function() { + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.DEFAULT + ); + assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); + }); }); }); }); @@ -387,84 +482,121 @@ describe("SignalProtocolStore", function() { return wrapDeferred(record.destroy()); }); - it ('does nothing', function() { - return store.processContactSyncVerificationState( - identifier, store.VerifiedStatus.DEFAULT, newIdentity - ).then(fetchRecord).then(function() { - // fetchRecord resolved so there is a record. - // Bad. - throw new Error("processContactSyncVerificationState should not save new records"); - }, function() { - assert.strictEqual(keychangeTriggered, 0); - }); + it('does nothing', function() { + return store + .processContactSyncVerificationState( + identifier, + store.VerifiedStatus.DEFAULT, + newIdentity + ) + .then(fetchRecord) + .then( + function() { + // fetchRecord resolved so there is a record. + // Bad. + throw new Error( + 'processContactSyncVerificationState should not save new records' + ); + }, + function() { + assert.strictEqual(keychangeTriggered, 0); + } + ); }); }); describe('when the record exists', function() { describe('when the existing key is different', function() { before(function() { record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - firstUse : true, - timestamp : Date.now(), - verified : store.VerifiedStatus.VERIFIED, - nonblockingApproval : false + id: identifier, + publicKey: testKey.pubKey, + firstUse: true, + timestamp: Date.now(), + verified: store.VerifiedStatus.VERIFIED, + nonblockingApproval: false, }); return wrapDeferred(record.save()); }); - it ('does not save the new identity (because this is a less secure state)', function() { - return store.processContactSyncVerificationState( - identifier, store.VerifiedStatus.DEFAULT, newIdentity - ).then(fetchRecord).then(function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.VERIFIED); - assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); - assert.strictEqual(keychangeTriggered, 0); - }); + it('does not save the new identity (because this is a less secure state)', function() { + return store + .processContactSyncVerificationState( + identifier, + store.VerifiedStatus.DEFAULT, + newIdentity + ) + .then(fetchRecord) + .then(function() { + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.VERIFIED + ); + assertEqualArrayBuffers( + record.get('publicKey'), + testKey.pubKey + ); + assert.strictEqual(keychangeTriggered, 0); + }); }); }); describe('when the existing key is the same but VERIFIED', function() { before(function() { record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - firstUse : true, - timestamp : Date.now(), - verified : store.VerifiedStatus.VERIFIED, - nonblockingApproval : false + id: identifier, + publicKey: testKey.pubKey, + firstUse: true, + timestamp: Date.now(), + verified: store.VerifiedStatus.VERIFIED, + nonblockingApproval: false, }); return wrapDeferred(record.save()); }); - it ('updates the verified status', function() { - return store.processContactSyncVerificationState( - identifier, store.VerifiedStatus.DEFAULT, testKey.pubKey - ).then(fetchRecord).then(function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.DEFAULT); - assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); - assert.strictEqual(keychangeTriggered, 0); - }); + it('updates the verified status', function() { + return store + .processContactSyncVerificationState( + identifier, + store.VerifiedStatus.DEFAULT, + testKey.pubKey + ) + .then(fetchRecord) + .then(function() { + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.DEFAULT + ); + assertEqualArrayBuffers( + record.get('publicKey'), + testKey.pubKey + ); + assert.strictEqual(keychangeTriggered, 0); + }); }); }); describe('when the existing key is the same and already DEFAULT', function() { before(function() { record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - firstUse : true, - timestamp : Date.now(), - verified : store.VerifiedStatus.DEFAULT, - nonblockingApproval : false + id: identifier, + publicKey: testKey.pubKey, + firstUse: true, + timestamp: Date.now(), + verified: store.VerifiedStatus.DEFAULT, + nonblockingApproval: false, }); return wrapDeferred(record.save()); }); - it ('does not hang', function() { - return store.processContactSyncVerificationState( - identifier, store.VerifiedStatus.DEFAULT, testKey.pubKey - ).then(fetchRecord).then(function() { - assert.strictEqual(keychangeTriggered, 0); - }); + it('does not hang', function() { + return store + .processContactSyncVerificationState( + identifier, + store.VerifiedStatus.DEFAULT, + testKey.pubKey + ) + .then(fetchRecord) + .then(function() { + assert.strictEqual(keychangeTriggered, 0); + }); }); }); }); @@ -476,82 +608,114 @@ describe("SignalProtocolStore", function() { return wrapDeferred(record.destroy()); }); - it ('saves the new identity and marks it verified', function() { - return store.processContactSyncVerificationState( - identifier, store.VerifiedStatus.UNVERIFIED, newIdentity - ).then(fetchRecord).then(function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.UNVERIFIED); - assertEqualArrayBuffers(record.get('publicKey'), newIdentity); - assert.strictEqual(keychangeTriggered, 0); - }); + it('saves the new identity and marks it verified', function() { + return store + .processContactSyncVerificationState( + identifier, + store.VerifiedStatus.UNVERIFIED, + newIdentity + ) + .then(fetchRecord) + .then(function() { + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.UNVERIFIED + ); + assertEqualArrayBuffers(record.get('publicKey'), newIdentity); + assert.strictEqual(keychangeTriggered, 0); + }); }); }); describe('when the record exists', function() { describe('when the existing key is different', function() { before(function() { record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - firstUse : true, - timestamp : Date.now(), - verified : store.VerifiedStatus.VERIFIED, - nonblockingApproval : false + id: identifier, + publicKey: testKey.pubKey, + firstUse: true, + timestamp: Date.now(), + verified: store.VerifiedStatus.VERIFIED, + nonblockingApproval: false, }); return wrapDeferred(record.save()); }); - it ('saves the new identity and marks it UNVERIFIED', function() { - return store.processContactSyncVerificationState( - identifier, store.VerifiedStatus.UNVERIFIED, newIdentity - ).then(fetchRecord).then(function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.UNVERIFIED); - assertEqualArrayBuffers(record.get('publicKey'), newIdentity); - assert.strictEqual(keychangeTriggered, 1); - }); + it('saves the new identity and marks it UNVERIFIED', function() { + return store + .processContactSyncVerificationState( + identifier, + store.VerifiedStatus.UNVERIFIED, + newIdentity + ) + .then(fetchRecord) + .then(function() { + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.UNVERIFIED + ); + assertEqualArrayBuffers(record.get('publicKey'), newIdentity); + assert.strictEqual(keychangeTriggered, 1); + }); }); }); describe('when the key exists and is DEFAULT', function() { before(function() { record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - firstUse : true, - timestamp : Date.now(), - verified : store.VerifiedStatus.DEFAULT, - nonblockingApproval : false + id: identifier, + publicKey: testKey.pubKey, + firstUse: true, + timestamp: Date.now(), + verified: store.VerifiedStatus.DEFAULT, + nonblockingApproval: false, }); return wrapDeferred(record.save()); }); - it ('updates the verified status', function() { - return store.processContactSyncVerificationState( - identifier, store.VerifiedStatus.UNVERIFIED, testKey.pubKey - ).then(fetchRecord).then(function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.UNVERIFIED); - assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); - assert.strictEqual(keychangeTriggered, 0); - }); + it('updates the verified status', function() { + return store + .processContactSyncVerificationState( + identifier, + store.VerifiedStatus.UNVERIFIED, + testKey.pubKey + ) + .then(fetchRecord) + .then(function() { + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.UNVERIFIED + ); + assertEqualArrayBuffers( + record.get('publicKey'), + testKey.pubKey + ); + assert.strictEqual(keychangeTriggered, 0); + }); }); }); describe('when the key exists and is already UNVERIFIED', function() { before(function() { record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - firstUse : true, - timestamp : Date.now(), - verified : store.VerifiedStatus.UNVERIFIED, - nonblockingApproval : false + id: identifier, + publicKey: testKey.pubKey, + firstUse: true, + timestamp: Date.now(), + verified: store.VerifiedStatus.UNVERIFIED, + nonblockingApproval: false, }); return wrapDeferred(record.save()); }); - it ('does not hang', function() { - return store.processContactSyncVerificationState( - identifier, store.VerifiedStatus.UNVERIFIED, testKey.pubKey - ).then(fetchRecord).then(function() { - assert.strictEqual(keychangeTriggered, 0); - }); + it('does not hang', function() { + return store + .processContactSyncVerificationState( + identifier, + store.VerifiedStatus.UNVERIFIED, + testKey.pubKey + ) + .then(fetchRecord) + .then(function() { + assert.strictEqual(keychangeTriggered, 0); + }); }); }); }); @@ -565,187 +729,241 @@ describe("SignalProtocolStore", function() { }); }); - it ('saves the new identity and marks it verified', function() { - return store.processContactSyncVerificationState( - identifier, store.VerifiedStatus.VERIFIED, newIdentity - ).then(fetchRecord).then(function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.VERIFIED); - assertEqualArrayBuffers(record.get('publicKey'), newIdentity); - assert.strictEqual(keychangeTriggered, 0); - }); + it('saves the new identity and marks it verified', function() { + return store + .processContactSyncVerificationState( + identifier, + store.VerifiedStatus.VERIFIED, + newIdentity + ) + .then(fetchRecord) + .then(function() { + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.VERIFIED + ); + assertEqualArrayBuffers(record.get('publicKey'), newIdentity); + assert.strictEqual(keychangeTriggered, 0); + }); }); }); describe('when the record exists', function() { describe('when the existing key is different', function() { before(function() { record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - firstUse : true, - timestamp : Date.now(), - verified : store.VerifiedStatus.VERIFIED, - nonblockingApproval : false + id: identifier, + publicKey: testKey.pubKey, + firstUse: true, + timestamp: Date.now(), + verified: store.VerifiedStatus.VERIFIED, + nonblockingApproval: false, }); return wrapDeferred(record.save()); }); - it ('saves the new identity and marks it VERIFIED', function() { - return store.processContactSyncVerificationState( - identifier, store.VerifiedStatus.VERIFIED, newIdentity - ).then(fetchRecord).then(function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.VERIFIED); - assertEqualArrayBuffers(record.get('publicKey'), newIdentity); - assert.strictEqual(keychangeTriggered, 1); - }); + it('saves the new identity and marks it VERIFIED', function() { + return store + .processContactSyncVerificationState( + identifier, + store.VerifiedStatus.VERIFIED, + newIdentity + ) + .then(fetchRecord) + .then(function() { + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.VERIFIED + ); + assertEqualArrayBuffers(record.get('publicKey'), newIdentity); + assert.strictEqual(keychangeTriggered, 1); + }); }); }); describe('when the existing key is the same but UNVERIFIED', function() { before(function() { record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - firstUse : true, - timestamp : Date.now(), - verified : store.VerifiedStatus.UNVERIFIED, - nonblockingApproval : false + id: identifier, + publicKey: testKey.pubKey, + firstUse: true, + timestamp: Date.now(), + verified: store.VerifiedStatus.UNVERIFIED, + nonblockingApproval: false, }); return wrapDeferred(record.save()); }); - it ('saves the identity and marks it verified', function() { - return store.processContactSyncVerificationState( - identifier, store.VerifiedStatus.VERIFIED, testKey.pubKey - ).then(fetchRecord).then(function() { - assert.strictEqual(record.get('verified'), store.VerifiedStatus.VERIFIED); - assertEqualArrayBuffers(record.get('publicKey'), testKey.pubKey); - assert.strictEqual(keychangeTriggered, 0); - }); + it('saves the identity and marks it verified', function() { + return store + .processContactSyncVerificationState( + identifier, + store.VerifiedStatus.VERIFIED, + testKey.pubKey + ) + .then(fetchRecord) + .then(function() { + assert.strictEqual( + record.get('verified'), + store.VerifiedStatus.VERIFIED + ); + assertEqualArrayBuffers( + record.get('publicKey'), + testKey.pubKey + ); + assert.strictEqual(keychangeTriggered, 0); + }); }); }); describe('when the existing key is the same and already VERIFIED', function() { before(function() { record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - firstUse : true, - timestamp : Date.now(), - verified : store.VerifiedStatus.VERIFIED, - nonblockingApproval : false + id: identifier, + publicKey: testKey.pubKey, + firstUse: true, + timestamp: Date.now(), + verified: store.VerifiedStatus.VERIFIED, + nonblockingApproval: false, }); return wrapDeferred(record.save()); }); - it ('does not hang', function() { - return store.processContactSyncVerificationState( - identifier, store.VerifiedStatus.VERIFIED, testKey.pubKey - ).then(fetchRecord).then(function() { - assert.strictEqual(keychangeTriggered, 0); - }); + it('does not hang', function() { + return store + .processContactSyncVerificationState( + identifier, + store.VerifiedStatus.VERIFIED, + testKey.pubKey + ) + .then(fetchRecord) + .then(function() { + assert.strictEqual(keychangeTriggered, 0); + }); }); }); }); }); - }); describe('isUntrusted', function() { it('returns false if identity key old enough', function() { var record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - timestamp : Date.now() - 10 * 1000 * 60, - verified : store.VerifiedStatus.DEFAULT, - firstUse : false, - nonblockingApproval : false - }); - return wrapDeferred(record.save()).then(function() { - return store.isUntrusted(identifier); - }).then(function(untrusted) { - assert.strictEqual(untrusted, false); + id: identifier, + publicKey: testKey.pubKey, + timestamp: Date.now() - 10 * 1000 * 60, + verified: store.VerifiedStatus.DEFAULT, + firstUse: false, + nonblockingApproval: false, }); + return wrapDeferred(record.save()) + .then(function() { + return store.isUntrusted(identifier); + }) + .then(function(untrusted) { + assert.strictEqual(untrusted, false); + }); }); it('returns false if new but nonblockingApproval is true', function() { var record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - timestamp : Date.now(), - verified : store.VerifiedStatus.DEFAULT, - firstUse : false, - nonblockingApproval : true - }); - return wrapDeferred(record.save()).then(function() { - return store.isUntrusted(identifier); - }).then(function(untrusted) { - assert.strictEqual(untrusted, false); + id: identifier, + publicKey: testKey.pubKey, + timestamp: Date.now(), + verified: store.VerifiedStatus.DEFAULT, + firstUse: false, + nonblockingApproval: true, }); + return wrapDeferred(record.save()) + .then(function() { + return store.isUntrusted(identifier); + }) + .then(function(untrusted) { + assert.strictEqual(untrusted, false); + }); }); it('returns false if new but firstUse is true', function() { var record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - timestamp : Date.now(), - verified : store.VerifiedStatus.DEFAULT, - firstUse : true, - nonblockingApproval : false - }); - return wrapDeferred(record.save()).then(function() { - return store.isUntrusted(identifier); - }).then(function(untrusted) { - assert.strictEqual(untrusted, false); + id: identifier, + publicKey: testKey.pubKey, + timestamp: Date.now(), + verified: store.VerifiedStatus.DEFAULT, + firstUse: true, + nonblockingApproval: false, }); + return wrapDeferred(record.save()) + .then(function() { + return store.isUntrusted(identifier); + }) + .then(function(untrusted) { + assert.strictEqual(untrusted, false); + }); }); it('returns true if new, and no flags are set', function() { var record = new IdentityKeyRecord({ - id : identifier, - publicKey : testKey.pubKey, - timestamp : Date.now(), - verified : store.VerifiedStatus.DEFAULT, - firstUse : false, - nonblockingApproval : false - }); - return wrapDeferred(record.save()).then(function() { - return store.isUntrusted(identifier); - }).then(function(untrusted) { - assert.strictEqual(untrusted, true); + id: identifier, + publicKey: testKey.pubKey, + timestamp: Date.now(), + verified: store.VerifiedStatus.DEFAULT, + firstUse: false, + nonblockingApproval: false, }); + return wrapDeferred(record.save()) + .then(function() { + return store.isUntrusted(identifier); + }) + .then(function(untrusted) { + assert.strictEqual(untrusted, true); + }); }); }); describe('getVerified', function() { before(function(done) { - store.setVerified(identifier, store.VerifiedStatus.VERIFIED).then(done, done); + store + .setVerified(identifier, store.VerifiedStatus.VERIFIED) + .then(done, done); }); - it ('resolves to the verified status', function(done) { - store.getVerified(identifier).then(function(result) { - assert.strictEqual(result, store.VerifiedStatus.VERIFIED); - }).then(done, done); + it('resolves to the verified status', function(done) { + store + .getVerified(identifier) + .then(function(result) { + assert.strictEqual(result, store.VerifiedStatus.VERIFIED); + }) + .then(done, done); }); }); describe('isTrustedIdentity', function() { var address = new libsignal.SignalProtocolAddress(identifier, 1); describe('When invalid direction is given', function(done) { it('should fail', function(done) { - store.isTrustedIdentity(identifier, testKey.pubKey).then(function() { - done(new Error('isTrustedIdentity should have failed')); - }).catch(function(e) { - done(); - }); + store + .isTrustedIdentity(identifier, testKey.pubKey) + .then(function() { + done(new Error('isTrustedIdentity should have failed')); + }) + .catch(function(e) { + done(); + }); }); }); describe('When direction is RECEIVING', function() { it('always returns true', function(done) { var newIdentity = libsignal.crypto.getRandomBytes(33); store.saveIdentity(address.toString(), testKey.pubKey).then(function() { - store.isTrustedIdentity(identifier, newIdentity, store.Direction.RECEIVING).then(function(trusted) { - if (trusted) { - done(); - } else { - done(new Error('isTrusted returned false when receiving')); - } - }).catch(done); + store + .isTrustedIdentity( + identifier, + newIdentity, + store.Direction.RECEIVING + ) + .then(function(trusted) { + if (trusted) { + done(); + } else { + done(new Error('isTrusted returned false when receiving')); + } + }) + .catch(done); }); }); }); @@ -758,59 +976,91 @@ describe("SignalProtocolStore", function() { }); it('returns true', function(done) { var newIdentity = libsignal.crypto.getRandomBytes(33); - store.isTrustedIdentity(identifier, newIdentity, store.Direction.SENDING).then(function(trusted) { - if (trusted) { - done(); - } else { - done(new Error('isTrusted returned false on first use')); - } - }).catch(done); + store + .isTrustedIdentity(identifier, newIdentity, store.Direction.SENDING) + .then(function(trusted) { + if (trusted) { + done(); + } else { + done(new Error('isTrusted returned false on first use')); + } + }) + .catch(done); }); }); describe('When there is an existing key', function() { before(function(done) { - store.saveIdentity(address.toString(), testKey.pubKey).then(function() { - done(); - }); + store + .saveIdentity(address.toString(), testKey.pubKey) + .then(function() { + done(); + }); }); describe('When the existing key is different', function() { it('returns false', function(done) { var newIdentity = libsignal.crypto.getRandomBytes(33); - store.isTrustedIdentity(identifier, newIdentity, store.Direction.SENDING).then(function(trusted) { - if (trusted) { - done(new Error('isTrusted returned true on untrusted key')); - } else { - done(); - } - }).catch(done); + store + .isTrustedIdentity( + identifier, + newIdentity, + store.Direction.SENDING + ) + .then(function(trusted) { + if (trusted) { + done(new Error('isTrusted returned true on untrusted key')); + } else { + done(); + } + }) + .catch(done); }); }); describe('When the existing key matches the new key', function() { var newIdentity = libsignal.crypto.getRandomBytes(33); before(function(done) { - store.saveIdentity(address.toString(), newIdentity).then(function() { - done(); - }); + store + .saveIdentity(address.toString(), newIdentity) + .then(function() { + done(); + }); }); it('returns false if keys match but we just received this new identiy', function(done) { - store.isTrustedIdentity(identifier, newIdentity, store.Direction.SENDING).then(function(trusted) { - if (trusted) { - done(new Error('isTrusted returned true on untrusted key')); - } else { - done(); - } - }).catch(done); + store + .isTrustedIdentity( + identifier, + newIdentity, + store.Direction.SENDING + ) + .then(function(trusted) { + if (trusted) { + done(new Error('isTrusted returned true on untrusted key')); + } else { + done(); + } + }) + .catch(done); }); it('returns true if we have already approved identity', function(done) { - store.saveIdentity(address.toString(), newIdentity, true).then(function() { - store.isTrustedIdentity(identifier, newIdentity, store.Direction.SENDING).then(function(trusted) { - if (trusted) { - done(); - } else { - done(new Error('isTrusted returned false on an approved key')); - } - }).catch(done); - }); + store + .saveIdentity(address.toString(), newIdentity, true) + .then(function() { + store + .isTrustedIdentity( + identifier, + newIdentity, + store.Direction.SENDING + ) + .then(function(trusted) { + if (trusted) { + done(); + } else { + done( + new Error('isTrusted returned false on an approved key') + ); + } + }) + .catch(done); + }); }); }); }); @@ -818,12 +1068,15 @@ describe("SignalProtocolStore", function() { }); describe('storePreKey', function() { it('stores prekeys', function(done) { - store.storePreKey(1, testKey).then(function() { - return store.loadPreKey(1).then(function(key) { - assertEqualArrayBuffers(key.pubKey, testKey.pubKey); - assertEqualArrayBuffers(key.privKey, testKey.privKey); - }); - }).then(done,done); + store + .storePreKey(1, testKey) + .then(function() { + return store.loadPreKey(1).then(function(key) { + assertEqualArrayBuffers(key.pubKey, testKey.pubKey); + assertEqualArrayBuffers(key.privKey, testKey.privKey); + }); + }) + .then(done, done); }); }); describe('removePreKey', function() { @@ -831,21 +1084,27 @@ describe("SignalProtocolStore", function() { store.storePreKey(2, testKey).then(done); }); it('deletes prekeys', function(done) { - store.removePreKey(2, testKey).then(function() { - return store.loadPreKey(2).then(function(key) { - assert.isUndefined(key); - }); - }).then(done,done); + store + .removePreKey(2, testKey) + .then(function() { + return store.loadPreKey(2).then(function(key) { + assert.isUndefined(key); + }); + }) + .then(done, done); }); }); describe('storeSignedPreKey', function() { it('stores signed prekeys', function(done) { - store.storeSignedPreKey(3, testKey).then(function() { - return store.loadSignedPreKey(3).then(function(key) { - assertEqualArrayBuffers(key.pubKey, testKey.pubKey); - assertEqualArrayBuffers(key.privKey, testKey.privKey); - }); - }).then(done,done); + store + .storeSignedPreKey(3, testKey) + .then(function() { + return store.loadSignedPreKey(3).then(function(key) { + assertEqualArrayBuffers(key.pubKey, testKey.pubKey); + assertEqualArrayBuffers(key.privKey, testKey.privKey); + }); + }) + .then(done, done); }); }); describe('removeSignedPreKey', function() { @@ -853,26 +1112,32 @@ describe("SignalProtocolStore", function() { store.storeSignedPreKey(4, testKey).then(done); }); it('deletes signed prekeys', function(done) { - store.removeSignedPreKey(4, testKey).then(function() { - return store.loadSignedPreKey(4).then(function(key) { - assert.isUndefined(key); - }); - }).then(done,done); + store + .removeSignedPreKey(4, testKey) + .then(function() { + return store.loadSignedPreKey(4).then(function(key) { + assert.isUndefined(key); + }); + }) + .then(done, done); }); }); describe('storeSession', function() { it('stores sessions', function(done) { - var testRecord = "an opaque string"; - store.storeSession(identifier + '.1', testRecord).then(function() { - return store.loadSession(identifier + '.1').then(function(record) { - assert.deepEqual(record, testRecord); - }); - }).then(done,done); + var testRecord = 'an opaque string'; + store + .storeSession(identifier + '.1', testRecord) + .then(function() { + return store.loadSession(identifier + '.1').then(function(record) { + assert.deepEqual(record, testRecord); + }); + }) + .then(done, done); }); }); describe('removeAllSessions', function() { it('removes all sessions for a number', function(done) { - var testRecord = "an opaque string"; + var testRecord = 'an opaque string'; var devices = [1, 2, 3].map(function(deviceId) { return [identifier, deviceId].join('.'); }); @@ -882,33 +1147,39 @@ describe("SignalProtocolStore", function() { return store.storeSession(encodedNumber, testRecord + encodedNumber); }); }); - promise.then(function() { - return store.removeAllSessions(identifier).then(function(record) { - return Promise.all(devices.map(store.loadSession.bind(store))).then(function(records) { - for (var i in records) { - assert.isUndefined(records[i]); - }; + promise + .then(function() { + return store.removeAllSessions(identifier).then(function(record) { + return Promise.all(devices.map(store.loadSession.bind(store))).then( + function(records) { + for (var i in records) { + assert.isUndefined(records[i]); + } + } + ); }); - }); - }).then(done,done); + }) + .then(done, done); }); }); describe('clearSessionStore', function() { - it ('clears the session store', function(done) { - var testRecord = "an opaque string"; - store.storeSession(identifier + '.1', testRecord).then(function() { - return store.clearSessionStore().then(function() { - return store.loadSession(identifier + '.1').then(function(record) { - assert.isUndefined(record); + it('clears the session store', function(done) { + var testRecord = 'an opaque string'; + store + .storeSession(identifier + '.1', testRecord) + .then(function() { + return store.clearSessionStore().then(function() { + return store.loadSession(identifier + '.1').then(function(record) { + assert.isUndefined(record); + }); }); - }); - }).then(done,done); - + }) + .then(done, done); }); }); describe('getDeviceIds', function() { it('returns deviceIds for a number', function(done) { - var testRecord = "an opaque string"; + var testRecord = 'an opaque string'; var devices = [1, 2, 3].map(function(deviceId) { return [identifier, deviceId].join('.'); }); @@ -918,72 +1189,92 @@ describe("SignalProtocolStore", function() { return store.storeSession(encodedNumber, testRecord + encodedNumber); }); }); - promise.then(function() { - return store.getDeviceIds(identifier).then(function(deviceIds) { - assert.sameMembers(deviceIds, [1, 2, 3]); - }); - }).then(done,done); + promise + .then(function() { + return store.getDeviceIds(identifier).then(function(deviceIds) { + assert.sameMembers(deviceIds, [1, 2, 3]); + }); + }) + .then(done, done); }); it('returns empty array for a number with no device ids', function(done) { - return store.getDeviceIds('foo').then(function(deviceIds) { - assert.sameMembers(deviceIds,[]); - }).then(done,done); + return store + .getDeviceIds('foo') + .then(function(deviceIds) { + assert.sameMembers(deviceIds, []); + }) + .then(done, done); }); }); describe('Not yet processed messages', function() { beforeEach(function() { - return store.getAllUnprocessed().then(function(items) { - return Promise.all(_.map(items, function(item) { - return store.removeUnprocessed(item.id); - })); - }).then(function() { - return store.getAllUnprocessed(); - }).then(function(items) { - assert.strictEqual(items.length, 0); - }); + return store + .getAllUnprocessed() + .then(function(items) { + return Promise.all( + _.map(items, function(item) { + return store.removeUnprocessed(item.id); + }) + ); + }) + .then(function() { + return store.getAllUnprocessed(); + }) + .then(function(items) { + assert.strictEqual(items.length, 0); + }); }); it('adds two and gets them back', function() { return Promise.all([ - store.addUnprocessed({id: 2, name: 'second', timestamp: 2}), - store.addUnprocessed({id: 3, name: 'third', timestamp: 3}), - store.addUnprocessed({id: 1, name: 'first', timestamp: 1}) - ]).then(function() { - return store.getAllUnprocessed(); - }).then(function(items) { - assert.strictEqual(items.length, 3); + store.addUnprocessed({ id: 2, name: 'second', timestamp: 2 }), + store.addUnprocessed({ id: 3, name: 'third', timestamp: 3 }), + store.addUnprocessed({ id: 1, name: 'first', timestamp: 1 }), + ]) + .then(function() { + return store.getAllUnprocessed(); + }) + .then(function(items) { + assert.strictEqual(items.length, 3); - // they are in the proper order because the collection comparator is 'timestamp' - assert.strictEqual(items[0].name, 'first'); - assert.strictEqual(items[1].name, 'second'); - assert.strictEqual(items[2].name, 'third'); - }); + // they are in the proper order because the collection comparator is 'timestamp' + assert.strictEqual(items[0].name, 'first'); + assert.strictEqual(items[1].name, 'second'); + assert.strictEqual(items[2].name, 'third'); + }); }); it('updateUnprocessed successfully updates only part of itme', function() { var id = 1; - return store.addUnprocessed({id: id, name: 'first', timestamp: 1}).then(function() { - return store.updateUnprocessed(id, {name: 'updated'}); - }).then(function() { - return store.getAllUnprocessed(); - }).then(function(items) { - assert.strictEqual(items.length, 1); - assert.strictEqual(items[0].name, 'updated'); - assert.strictEqual(items[0].timestamp, 1); - }); + return store + .addUnprocessed({ id: id, name: 'first', timestamp: 1 }) + .then(function() { + return store.updateUnprocessed(id, { name: 'updated' }); + }) + .then(function() { + return store.getAllUnprocessed(); + }) + .then(function(items) { + assert.strictEqual(items.length, 1); + assert.strictEqual(items[0].name, 'updated'); + assert.strictEqual(items[0].timestamp, 1); + }); }); it('removeUnprocessed successfully deletes item', function() { var id = 1; - return store.addUnprocessed({id: id, name: 'first', timestamp: 1}).then(function() { - return store.removeUnprocessed(id); - }).then(function() { - return store.getAllUnprocessed(); - }).then(function(items) { - assert.strictEqual(items.length, 0); - }); + return store + .addUnprocessed({ id: id, name: 'first', timestamp: 1 }) + .then(function() { + return store.removeUnprocessed(id); + }) + .then(function() { + return store.getAllUnprocessed(); + }) + .then(function(items) { + assert.strictEqual(items.length, 0); + }); }); }); - }); diff --git a/test/styleguide/legacy_bridge.js b/test/styleguide/legacy_bridge.js index 33fc433f4..7cd0f9ff9 100644 --- a/test/styleguide/legacy_bridge.js +++ b/test/styleguide/legacy_bridge.js @@ -30,22 +30,25 @@ window.Signal.Backup = {}; window.Signal.Crypto = {}; window.Signal.Logs = {}; window.Signal.Migrations = { - getPlaceholderMigrations: () => [{ - migrate: (transaction, next) => { - console.log('migration version 1'); - transaction.db.createObjectStore('conversations'); - next(); + getPlaceholderMigrations: () => [ + { + migrate: (transaction, next) => { + console.log('migration version 1'); + transaction.db.createObjectStore('conversations'); + next(); + }, + version: 1, }, - version: 1, - }, { - migrate: (transaction, next) => { - console.log('migration version 2'); - const messages = transaction.db.createObjectStore('messages'); - messages.createIndex('expires_at', 'expireTimer', { unique: false }); - next(); + { + migrate: (transaction, next) => { + console.log('migration version 2'); + const messages = transaction.db.createObjectStore('messages'); + messages.createIndex('expires_at', 'expireTimer', { unique: false }); + next(); + }, + version: 2, }, - version: 2, - }], + ], loadAttachmentData: attachment => Promise.resolve(attachment), }; diff --git a/test/views/conversation_search_view_test.js b/test/views/conversation_search_view_test.js index 00f6d8b48..413a00f76 100644 --- a/test/views/conversation_search_view_test.js +++ b/test/views/conversation_search_view_test.js @@ -1,105 +1,119 @@ describe('ConversationSearchView', function() { it('should match partial numbers', function() { var $el = $('
'); - var view = new Whisper.ConversationSearchView({el: $el, input: $('')}).render(); + var view = new Whisper.ConversationSearchView({ + el: $el, + input: $(''), + }).render(); var maybe_numbers = [ - "+1 415", - "+1415", - "+1415", - "415", - "(415)", - " (415", - "(415) 123 4567", - "+1 (415) 123 4567", - " +1 (415) 123 4567", - "1 (415) 123 4567", - "1 415-123-4567", - "415-123-4567" + '+1 415', + '+1415', + '+1415', + '415', + '(415)', + ' (415', + '(415) 123 4567', + '+1 (415) 123 4567', + ' +1 (415) 123 4567', + '1 (415) 123 4567', + '1 415-123-4567', + '415-123-4567', ]; maybe_numbers.forEach(function(n) { assert.ok(view.maybeNumber(n), n); }); }); describe('Searching for left groups', function() { - var convo = new Whisper.ConversationCollection().add({ - id: 'a-left-group', - name: 'i left this group', - members: [], - type: 'group', - left: true - }); - before(function(done) { - convo.save().then(done); - }); - describe('with no messages', function() { - var input = $(''); - var view = new Whisper.ConversationSearchView({ input: input }).render(); - before(function(done) { - view.$input.val('left'); - view.filterContacts(); - view.typeahead_view.collection.on('reset', function() { - done(); - }); - }); - it('should not surface left groups with no messages', function() { - assert.isUndefined(view.typeahead_view.collection.get(convo.id), 'got left group'); - }); - }); - describe('with messages', function() { - var input = $(''); - var view = new Whisper.ConversationSearchView({ input: input }).render(); - before(function(done) { - convo.save({lastMessage: 'asdf'}).then(function() { - view.$input.val('left'); - view.filterContacts(); - view.typeahead_view.collection.on('reset', function() { - done(); - }); - }); - }); - it('should surface left groups with messages', function() { - assert.isDefined(view.typeahead_view.collection.get(convo.id), 'got left group'); - }); - }); - }); - describe('Showing all contacts', function() { + var convo = new Whisper.ConversationCollection().add({ + id: 'a-left-group', + name: 'i left this group', + members: [], + type: 'group', + left: true, + }); + before(function(done) { + convo.save().then(done); + }); + describe('with no messages', function() { var input = $(''); var view = new Whisper.ConversationSearchView({ input: input }).render(); - view.showAllContacts = true; - var convo = new Whisper.ConversationCollection().add({ - id: 'a-left-group', - name: 'i left this group', - members: [], - type: 'group', - left: true - }); before(function(done) { - convo.save().then(done); + view.$input.val('left'); + view.filterContacts(); + view.typeahead_view.collection.on('reset', function() { + done(); + }); }); - describe('with no messages', function() { - before(function(done) { - view.resetTypeahead(); - view.typeahead_view.collection.on('reset', function() { - done(); - }); - }); - it('should not surface left groups with no messages', function() { - assert.isUndefined(view.typeahead_view.collection.get(convo.id), 'got left group'); - }); + it('should not surface left groups with no messages', function() { + assert.isUndefined( + view.typeahead_view.collection.get(convo.id), + 'got left group' + ); }); - describe('with messages', function() { - before(function(done) { - convo.save({lastMessage: 'asdf'}).then(function() { - view.typeahead_view.collection.on('reset', function() { - done(); - }); - view.resetTypeahead(); - }); - }); - it('should surface left groups with messages', function() { - assert.isDefined(view.typeahead_view.collection.get(convo.id), 'got left group'); + }); + describe('with messages', function() { + var input = $(''); + var view = new Whisper.ConversationSearchView({ input: input }).render(); + before(function(done) { + convo.save({ lastMessage: 'asdf' }).then(function() { + view.$input.val('left'); + view.filterContacts(); + view.typeahead_view.collection.on('reset', function() { + done(); }); + }); }); + it('should surface left groups with messages', function() { + assert.isDefined( + view.typeahead_view.collection.get(convo.id), + 'got left group' + ); + }); + }); + }); + describe('Showing all contacts', function() { + var input = $(''); + var view = new Whisper.ConversationSearchView({ input: input }).render(); + view.showAllContacts = true; + var convo = new Whisper.ConversationCollection().add({ + id: 'a-left-group', + name: 'i left this group', + members: [], + type: 'group', + left: true, + }); + before(function(done) { + convo.save().then(done); + }); + describe('with no messages', function() { + before(function(done) { + view.resetTypeahead(); + view.typeahead_view.collection.on('reset', function() { + done(); + }); + }); + it('should not surface left groups with no messages', function() { + assert.isUndefined( + view.typeahead_view.collection.get(convo.id), + 'got left group' + ); + }); + }); + describe('with messages', function() { + before(function(done) { + convo.save({ lastMessage: 'asdf' }).then(function() { + view.typeahead_view.collection.on('reset', function() { + done(); + }); + view.resetTypeahead(); + }); + }); + it('should surface left groups with messages', function() { + assert.isDefined( + view.typeahead_view.collection.get(convo.id), + 'got left group' + ); + }); + }); }); - }); diff --git a/test/views/group_update_view_test.js b/test/views/group_update_view_test.js index e49eeed6d..dd3d06d4c 100644 --- a/test/views/group_update_view_test.js +++ b/test/views/group_update_view_test.js @@ -1,16 +1,22 @@ describe('GroupUpdateView', function() { it('should show new group members', function() { - var view = new Whisper.GroupUpdateView({model: {joined: ['Alice', 'Bob']}}).render(); + var view = new Whisper.GroupUpdateView({ + model: { joined: ['Alice', 'Bob'] }, + }).render(); assert.match(view.$el.text(), /Alice.*Bob.*joined the group/); }); it('should note updates to the title', function() { - var view = new Whisper.GroupUpdateView({model: {name: 'New name'}}).render(); + var view = new Whisper.GroupUpdateView({ + model: { name: 'New name' }, + }).render(); assert.match(view.$el.text(), /Title is now 'New name'/); }); it('should say "Updated the group"', function() { - var view = new Whisper.GroupUpdateView({model: {avatar: 'New avatar'}}).render(); + var view = new Whisper.GroupUpdateView({ + model: { avatar: 'New avatar' }, + }).render(); assert.match(view.$el.text(), /Updated the group/); }); }); diff --git a/test/views/inbox_view_test.js b/test/views/inbox_view_test.js index 341e09a45..1eb05a7ec 100644 --- a/test/views/inbox_view_test.js +++ b/test/views/inbox_view_test.js @@ -1,41 +1,41 @@ describe('InboxView', function() { - var inboxView = new Whisper.InboxView({ - model: {}, - window: window, - initialLoadComplete: function() {} - }).render(); + var inboxView = new Whisper.InboxView({ + model: {}, + window: window, + initialLoadComplete: function() {}, + }).render(); - var conversation = new Whisper.Conversation({ id: '1234', type: 'private'}); + var conversation = new Whisper.Conversation({ id: '1234', type: 'private' }); - describe('the conversation stack', function() { - it('should be rendered', function() { - assert.ok(inboxView.$('.conversation-stack').length === 1); - }); - - describe('opening a conversation', function() { - var triggeredOpenedCount = 0; - - before(function() { - conversation.on('opened', function() { - triggeredOpenedCount++; - }); - - inboxView.conversation_stack.open(conversation); - }); - - it('should trigger an opened event', function() { - assert.ok(triggeredOpenedCount === 1); - }); - - describe('and then opening it again immediately', function() { - before(function() { - inboxView.conversation_stack.open(conversation); - }); - - it('should trigger the opened event again', function() { - assert.ok(triggeredOpenedCount === 2); - }); - }); - }); + describe('the conversation stack', function() { + it('should be rendered', function() { + assert.ok(inboxView.$('.conversation-stack').length === 1); }); + + describe('opening a conversation', function() { + var triggeredOpenedCount = 0; + + before(function() { + conversation.on('opened', function() { + triggeredOpenedCount++; + }); + + inboxView.conversation_stack.open(conversation); + }); + + it('should trigger an opened event', function() { + assert.ok(triggeredOpenedCount === 1); + }); + + describe('and then opening it again immediately', function() { + before(function() { + inboxView.conversation_stack.open(conversation); + }); + + it('should trigger the opened event again', function() { + assert.ok(triggeredOpenedCount === 2); + }); + }); + }); + }); }); diff --git a/test/views/last_seen_indicator_view_test.js b/test/views/last_seen_indicator_view_test.js index 667ceb716..3339ac32c 100644 --- a/test/views/last_seen_indicator_view_test.js +++ b/test/views/last_seen_indicator_view_test.js @@ -2,33 +2,32 @@ * vim: ts=4:sw=4:expandtab */ describe('LastSeenIndicatorView', function() { - it('renders provided count', function() { - var view = new Whisper.LastSeenIndicatorView({count: 10}); - assert.equal(view.count, 10); + it('renders provided count', function() { + var view = new Whisper.LastSeenIndicatorView({ count: 10 }); + assert.equal(view.count, 10); - view.render(); - assert.match(view.$el.html(), /10 Unread Messages/); - }); + view.render(); + assert.match(view.$el.html(), /10 Unread Messages/); + }); - it('renders count of 1', function() { - var view = new Whisper.LastSeenIndicatorView({count: 1}); - assert.equal(view.count, 1); + it('renders count of 1', function() { + var view = new Whisper.LastSeenIndicatorView({ count: 1 }); + assert.equal(view.count, 1); - view.render(); - assert.match(view.$el.html(), /1 Unread Message/); - }); + view.render(); + assert.match(view.$el.html(), /1 Unread Message/); + }); - it('increments count', function() { - var view = new Whisper.LastSeenIndicatorView({count: 4}); + it('increments count', function() { + var view = new Whisper.LastSeenIndicatorView({ count: 4 }); - assert.equal(view.count, 4); - view.render(); - assert.match(view.$el.html(), /4 Unread Messages/); - - view.increment(3); - assert.equal(view.count, 7); - view.render(); - assert.match(view.$el.html(), /7 Unread Messages/); - }); + assert.equal(view.count, 4); + view.render(); + assert.match(view.$el.html(), /4 Unread Messages/); + view.increment(3); + assert.equal(view.count, 7); + view.render(); + assert.match(view.$el.html(), /7 Unread Messages/); + }); }); diff --git a/test/views/list_view_test.js b/test/views/list_view_test.js index 22999cbfd..8ba47d170 100644 --- a/test/views/list_view_test.js +++ b/test/views/list_view_test.js @@ -6,7 +6,7 @@ describe('ListView', function() { }); it('should add children to the list element as they are added to the collection', function() { - var view = new Whisper.ListView({collection: collection}); + var view = new Whisper.ListView({ collection: collection }); collection.add('hello'); assert.equal(view.$el.children().length, 1); collection.add('world'); @@ -14,9 +14,8 @@ describe('ListView', function() { }); it('should add all the children to the list element on reset', function() { - var view = new Whisper.ListView({collection: collection}); + var view = new Whisper.ListView({ collection: collection }); collection.reset(['goodbye', 'world']); assert.equal(view.$el.children().length, 2); }); - }); diff --git a/test/views/message_view_test.js b/test/views/message_view_test.js index e9106af51..14556b45d 100644 --- a/test/views/message_view_test.js +++ b/test/views/message_view_test.js @@ -1,9 +1,9 @@ describe('MessageView', function() { var convo, message; - before(async (done) => { + before(async done => { await clearDatabase(); - convo = new Whisper.Conversation({id: 'foo'}); + convo = new Whisper.Conversation({ id: 'foo' }); message = convo.messageCollection.add({ conversationId: convo.id, body: 'hello world', @@ -17,39 +17,39 @@ describe('MessageView', function() { }); it('should display the message text', function() { - var view = new Whisper.MessageView({model: message}).render(); + var view = new Whisper.MessageView({ model: message }).render(); assert.match(view.$el.text(), /hello world/); }); it('should auto-update the message text', function() { - var view = new Whisper.MessageView({model: message}).render(); + var view = new Whisper.MessageView({ model: message }).render(); message.set('body', 'goodbye world'); assert.match(view.$el.html(), /goodbye world/); }); it('should have a nice timestamp', function() { - var view = new Whisper.MessageView({model: message}); - message.set({'sent_at': Date.now() - 5000}); + var view = new Whisper.MessageView({ model: message }); + message.set({ sent_at: Date.now() - 5000 }); view.render(); assert.match(view.$el.html(), /now/); - message.set({'sent_at': Date.now() - 60000}); + message.set({ sent_at: Date.now() - 60000 }); view.render(); assert.match(view.$el.html(), /min/); - message.set({'sent_at': Date.now() - 3600000}); + message.set({ sent_at: Date.now() - 3600000 }); view.render(); assert.match(view.$el.html(), /hour/); }); it('should not imply messages are from the future', function() { - var view = new Whisper.MessageView({model: message}); - message.set({'sent_at': Date.now() + 60000}); + var view = new Whisper.MessageView({ model: message }); + message.set({ sent_at: Date.now() + 60000 }); view.render(); assert.match(view.$el.html(), /now/); }); it('should go away when the model is destroyed', function() { - var view = new Whisper.MessageView({model: message}); + var view = new Whisper.MessageView({ model: message }); var div = $('
').append(view.$el); message.destroy(); assert.strictEqual(div.find(view.$el).length, 0); @@ -58,7 +58,7 @@ describe('MessageView', function() { it('allows links', function() { var url = 'http://example.com'; message.set('body', url); - var view = new Whisper.MessageView({model: message}); + var view = new Whisper.MessageView({ model: message }); view.render(); var link = view.$el.find('.body a'); assert.strictEqual(link.length, 1); @@ -69,7 +69,7 @@ describe('MessageView', function() { it('disallows xss', function() { var xss = ''; message.set('body', xss); - var view = new Whisper.MessageView({model: message}); + var view = new Whisper.MessageView({ model: message }); view.render(); assert.include(view.$el.text(), xss); // should appear as escaped text assert.strictEqual(view.$el.find('script').length, 0); // should not appear as html @@ -77,11 +77,14 @@ describe('MessageView', function() { it('supports emoji', function() { message.set('body', 'I \u2764\uFE0F emoji!'); - var view = new Whisper.MessageView({model: message}); + var view = new Whisper.MessageView({ model: message }); view.render(); var img = view.$el.find('.content img'); assert.strictEqual(img.length, 1); - assert.strictEqual(img.attr('src'), 'node_modules/emoji-datasource-apple/img/apple/64/2764-fe0f.png'); + assert.strictEqual( + img.attr('src'), + 'node_modules/emoji-datasource-apple/img/apple/64/2764-fe0f.png' + ); assert.strictEqual(img.attr('title'), ':heart:'); assert.strictEqual(img.attr('class'), 'emoji'); }); diff --git a/test/views/network_status_view_test.js b/test/views/network_status_view_test.js index 2834c7c02..d4f5e5798 100644 --- a/test/views/network_status_view_test.js +++ b/test/views/network_status_view_test.js @@ -1,163 +1,186 @@ - describe('NetworkStatusView', function() { - describe('getNetworkStatus', function() { - var networkStatusView; - var socketStatus = WebSocket.OPEN; + describe('getNetworkStatus', function() { + var networkStatusView; + var socketStatus = WebSocket.OPEN; - var oldGetSocketStatus; + var oldGetSocketStatus; - /* BEGIN stubbing globals */ - before(function() { - oldGetSocketStatus = window.getSocketStatus; - window.getSocketStatus = function() { return socketStatus; }; - }); - - after(function() { - window.getSocketStatus = oldGetSocketStatus; - - // It turns out that continued calls to window.getSocketStatus happen - // because we host NetworkStatusView in three mock interfaces, and the view - // checks every N seconds. That results in infinite errors unless there is - // something to call. - window.getSocketStatus = function() { return WebSocket.OPEN; }; - }); - /* END stubbing globals */ - - beforeEach(function() { - networkStatusView = new Whisper.NetworkStatusView(); - $('.network-status-container').append(networkStatusView.el); - }); - afterEach(function() { - // prevents huge number of errors on console after running tests - clearInterval(networkStatusView.renderIntervalHandle); - networkStatusView = null; - }); - - describe('initialization', function() { - it('should have an empty interval', function() { - assert.equal(networkStatusView.socketReconnectWaitDuration.asSeconds(), 0); - }); - }); - describe('network status with no connection', function() { - beforeEach(function() { - networkStatusView.navigatorOnLine = function() { return false; }; - }); - it('should be interrupted', function() { - networkStatusView.update(); - var status = networkStatusView.getNetworkStatus(); - assert(status.hasInterruption); - assert.equal(status.instructions, "Check your network connection."); - }); - it('should display an offline message', function() { - networkStatusView.update(); - assert.match(networkStatusView.$el.text(), /Offline/); - }); - it('should override socket status', function() { - _([WebSocket.CONNECTING, - WebSocket.OPEN, - WebSocket.CLOSING, - WebSocket.CLOSED]).map(function(socketStatusVal) { - socketStatus = socketStatusVal; - networkStatusView.update(); - assert.match(networkStatusView.$el.text(), /Offline/); - }); - }); - it('should override registration status', function() { - Whisper.Registration.remove(); - networkStatusView.update(); - assert.match(networkStatusView.$el.text(), /Offline/); - }); - }); - describe('network status when registration is not done', function() { - beforeEach(function() { - Whisper.Registration.remove(); - }); - it('should display an unlinked message', function() { - networkStatusView.update(); - assert.match(networkStatusView.$el.text(), /Relink/); - }); - it('should override socket status', function() { - _([WebSocket.CONNECTING, - WebSocket.OPEN, - WebSocket.CLOSING, - WebSocket.CLOSED]).map(function(socketStatusVal) { - socketStatus = socketStatusVal; - networkStatusView.update(); - assert.match(networkStatusView.$el.text(), /Relink/); - }); - }); - }); - describe('network status when registration is done', function() { - beforeEach(function() { - networkStatusView.navigatorOnLine = function() { return true; }; - Whisper.Registration.markDone(); - networkStatusView.update(); - }); - it('should not display an unlinked message', function() { - networkStatusView.update(); - assert.notMatch(networkStatusView.$el.text(), /Relink/); - }); - }); - describe('network status when socket is connecting', function() { - beforeEach(function() { - Whisper.Registration.markDone(); - socketStatus = WebSocket.CONNECTING; - networkStatusView.update(); - }); - it('it should display a connecting string if connecting and not in the connecting grace period', function() { - networkStatusView.withinConnectingGracePeriod = false; - var status = networkStatusView.getNetworkStatus(); - - assert.match(networkStatusView.$el.text(), /Connecting/); - }); - it('it should not be interrupted if in connecting grace period', function() { - assert(networkStatusView.withinConnectingGracePeriod); - var status = networkStatusView.getNetworkStatus(); - - assert.match(networkStatusView.$el.text(), /Connecting/); - assert(!status.hasInterruption); - }); - it('it should be interrupted if connecting grace period is over', function() { - networkStatusView.withinConnectingGracePeriod = false; - var status = networkStatusView.getNetworkStatus(); - - assert(status.hasInterruption); - }); - }); - describe('network status when socket is open', function() { - before(function() { - socketStatus = WebSocket.OPEN; - }); - it('should not be interrupted', function() { - var status = networkStatusView.getNetworkStatus(); - assert(!status.hasInterruption); - assert.match(networkStatusView.$el.find('.network-status-message').text().trim(), /^$/); - }); - }); - describe('network status when socket is closed or closing', function() { - _([WebSocket.CLOSED, WebSocket.CLOSING]).map(function(socketStatusVal) { - it('should be interrupted', function() { - socketStatus = socketStatusVal; - networkStatusView.update(); - var status = networkStatusView.getNetworkStatus(); - assert(status.hasInterruption); - }); - - }); - }); - describe('the socket reconnect interval', function() { - beforeEach(function() { - socketStatus = WebSocket.CLOSED; - networkStatusView.setSocketReconnectInterval(61000); - networkStatusView.update(); - }); - it('should format the message based on the socketReconnectWaitDuration property', function() { - assert.equal(networkStatusView.socketReconnectWaitDuration.asSeconds(), 61); - assert.match(networkStatusView.$('.network-status-message:last').text(), /Attempting reconnect/); - }); - it('should be reset by changing the socketStatus to CONNECTING', function() { - - }); - }); + /* BEGIN stubbing globals */ + before(function() { + oldGetSocketStatus = window.getSocketStatus; + window.getSocketStatus = function() { + return socketStatus; + }; }); + + after(function() { + window.getSocketStatus = oldGetSocketStatus; + + // It turns out that continued calls to window.getSocketStatus happen + // because we host NetworkStatusView in three mock interfaces, and the view + // checks every N seconds. That results in infinite errors unless there is + // something to call. + window.getSocketStatus = function() { + return WebSocket.OPEN; + }; + }); + /* END stubbing globals */ + + beforeEach(function() { + networkStatusView = new Whisper.NetworkStatusView(); + $('.network-status-container').append(networkStatusView.el); + }); + afterEach(function() { + // prevents huge number of errors on console after running tests + clearInterval(networkStatusView.renderIntervalHandle); + networkStatusView = null; + }); + + describe('initialization', function() { + it('should have an empty interval', function() { + assert.equal( + networkStatusView.socketReconnectWaitDuration.asSeconds(), + 0 + ); + }); + }); + describe('network status with no connection', function() { + beforeEach(function() { + networkStatusView.navigatorOnLine = function() { + return false; + }; + }); + it('should be interrupted', function() { + networkStatusView.update(); + var status = networkStatusView.getNetworkStatus(); + assert(status.hasInterruption); + assert.equal(status.instructions, 'Check your network connection.'); + }); + it('should display an offline message', function() { + networkStatusView.update(); + assert.match(networkStatusView.$el.text(), /Offline/); + }); + it('should override socket status', function() { + _([ + WebSocket.CONNECTING, + WebSocket.OPEN, + WebSocket.CLOSING, + WebSocket.CLOSED, + ]).map(function(socketStatusVal) { + socketStatus = socketStatusVal; + networkStatusView.update(); + assert.match(networkStatusView.$el.text(), /Offline/); + }); + }); + it('should override registration status', function() { + Whisper.Registration.remove(); + networkStatusView.update(); + assert.match(networkStatusView.$el.text(), /Offline/); + }); + }); + describe('network status when registration is not done', function() { + beforeEach(function() { + Whisper.Registration.remove(); + }); + it('should display an unlinked message', function() { + networkStatusView.update(); + assert.match(networkStatusView.$el.text(), /Relink/); + }); + it('should override socket status', function() { + _([ + WebSocket.CONNECTING, + WebSocket.OPEN, + WebSocket.CLOSING, + WebSocket.CLOSED, + ]).map(function(socketStatusVal) { + socketStatus = socketStatusVal; + networkStatusView.update(); + assert.match(networkStatusView.$el.text(), /Relink/); + }); + }); + }); + describe('network status when registration is done', function() { + beforeEach(function() { + networkStatusView.navigatorOnLine = function() { + return true; + }; + Whisper.Registration.markDone(); + networkStatusView.update(); + }); + it('should not display an unlinked message', function() { + networkStatusView.update(); + assert.notMatch(networkStatusView.$el.text(), /Relink/); + }); + }); + describe('network status when socket is connecting', function() { + beforeEach(function() { + Whisper.Registration.markDone(); + socketStatus = WebSocket.CONNECTING; + networkStatusView.update(); + }); + it('it should display a connecting string if connecting and not in the connecting grace period', function() { + networkStatusView.withinConnectingGracePeriod = false; + var status = networkStatusView.getNetworkStatus(); + + assert.match(networkStatusView.$el.text(), /Connecting/); + }); + it('it should not be interrupted if in connecting grace period', function() { + assert(networkStatusView.withinConnectingGracePeriod); + var status = networkStatusView.getNetworkStatus(); + + assert.match(networkStatusView.$el.text(), /Connecting/); + assert(!status.hasInterruption); + }); + it('it should be interrupted if connecting grace period is over', function() { + networkStatusView.withinConnectingGracePeriod = false; + var status = networkStatusView.getNetworkStatus(); + + assert(status.hasInterruption); + }); + }); + describe('network status when socket is open', function() { + before(function() { + socketStatus = WebSocket.OPEN; + }); + it('should not be interrupted', function() { + var status = networkStatusView.getNetworkStatus(); + assert(!status.hasInterruption); + assert.match( + networkStatusView.$el + .find('.network-status-message') + .text() + .trim(), + /^$/ + ); + }); + }); + describe('network status when socket is closed or closing', function() { + _([WebSocket.CLOSED, WebSocket.CLOSING]).map(function(socketStatusVal) { + it('should be interrupted', function() { + socketStatus = socketStatusVal; + networkStatusView.update(); + var status = networkStatusView.getNetworkStatus(); + assert(status.hasInterruption); + }); + }); + }); + describe('the socket reconnect interval', function() { + beforeEach(function() { + socketStatus = WebSocket.CLOSED; + networkStatusView.setSocketReconnectInterval(61000); + networkStatusView.update(); + }); + it('should format the message based on the socketReconnectWaitDuration property', function() { + assert.equal( + networkStatusView.socketReconnectWaitDuration.asSeconds(), + 61 + ); + assert.match( + networkStatusView.$('.network-status-message:last').text(), + /Attempting reconnect/ + ); + }); + it('should be reset by changing the socketStatus to CONNECTING', function() {}); + }); + }); }); diff --git a/test/views/scroll_down_button_view_test.js b/test/views/scroll_down_button_view_test.js index eb8787ad0..7d1ccc6f2 100644 --- a/test/views/scroll_down_button_view_test.js +++ b/test/views/scroll_down_button_view_test.js @@ -2,37 +2,37 @@ * vim: ts=4:sw=4:expandtab */ describe('ScrollDownButtonView', function() { - it('renders with count = 0', function() { - var view = new Whisper.ScrollDownButtonView(); - view.render(); - assert.equal(view.count, 0); - assert.match(view.$el.html(), /Scroll to bottom/); - }); + it('renders with count = 0', function() { + var view = new Whisper.ScrollDownButtonView(); + view.render(); + assert.equal(view.count, 0); + assert.match(view.$el.html(), /Scroll to bottom/); + }); - it('renders with count = 1', function() { - var view = new Whisper.ScrollDownButtonView({count: 1}); - view.render(); - assert.equal(view.count, 1); - assert.match(view.$el.html(), /new-messages/); - assert.match(view.$el.html(), /New message below/); - }); + it('renders with count = 1', function() { + var view = new Whisper.ScrollDownButtonView({ count: 1 }); + view.render(); + assert.equal(view.count, 1); + assert.match(view.$el.html(), /new-messages/); + assert.match(view.$el.html(), /New message below/); + }); - it('renders with count = 2', function() { - var view = new Whisper.ScrollDownButtonView({count: 2}); - view.render(); - assert.equal(view.count, 2); + it('renders with count = 2', function() { + var view = new Whisper.ScrollDownButtonView({ count: 2 }); + view.render(); + assert.equal(view.count, 2); - assert.match(view.$el.html(), /new-messages/); - assert.match(view.$el.html(), /New messages below/); - }); + assert.match(view.$el.html(), /new-messages/); + assert.match(view.$el.html(), /New messages below/); + }); - it('increments count and re-renders', function() { - var view = new Whisper.ScrollDownButtonView(); - view.render(); - assert.equal(view.count, 0); - assert.notMatch(view.$el.html(), /new-messages/); - view.increment(1); - assert.equal(view.count, 1); - assert.match(view.$el.html(), /new-messages/); - }); + it('increments count and re-renders', function() { + var view = new Whisper.ScrollDownButtonView(); + view.render(); + assert.equal(view.count, 0); + assert.notMatch(view.$el.html(), /new-messages/); + view.increment(1); + assert.equal(view.count, 1); + assert.match(view.$el.html(), /new-messages/); + }); }); diff --git a/test/views/threads_test.js b/test/views/threads_test.js index b0dcf3df2..729d8be4a 100644 --- a/test/views/threads_test.js +++ b/test/views/threads_test.js @@ -3,12 +3,11 @@ */ describe('Threads', function() { - it('should be ordered newest to oldest', function() { // Timestamps var today = new Date(); var tomorrow = new Date(); - tomorrow.setDate(today.getDate()+1); + tomorrow.setDate(today.getDate() + 1); // Add threads Whisper.Threads.add({ timestamp: today }); @@ -21,6 +20,4 @@ describe('Threads', function() { // Compare timestamps assert(firstTimestamp > secondTimestamp); }); - - }); diff --git a/test/views/timestamp_view_test.js b/test/views/timestamp_view_test.js index bf426926f..994830b37 100644 --- a/test/views/timestamp_view_test.js +++ b/test/views/timestamp_view_test.js @@ -4,122 +4,133 @@ 'use strict'; describe('TimestampView', function() { - it('formats long-ago timestamps correctly', function() { - var timestamp = Date.now(); - var brief_view = new Whisper.TimestampView({brief: true}).render(), - ext_view = new Whisper.ExtendedTimestampView().render(); + it('formats long-ago timestamps correctly', function() { + var timestamp = Date.now(); + var brief_view = new Whisper.TimestampView({ brief: true }).render(), + ext_view = new Whisper.ExtendedTimestampView().render(); - // Helper functions to check absolute and relative timestamps + // Helper functions to check absolute and relative timestamps - // Helper to check an absolute TS for an exact match - var check = function(view, ts, expected) { - var result = view.getRelativeTimeSpanString(ts); - assert.strictEqual(result, expected); - }; + // Helper to check an absolute TS for an exact match + var check = function(view, ts, expected) { + var result = view.getRelativeTimeSpanString(ts); + assert.strictEqual(result, expected); + }; - // Helper to check relative times for an exact match against both views - var checkDiff = function(sec_ago, expected_brief, expected_ext) { - check(brief_view, timestamp - sec_ago * 1000, expected_brief); - check(ext_view, timestamp - sec_ago * 1000, expected_ext); - }; + // Helper to check relative times for an exact match against both views + var checkDiff = function(sec_ago, expected_brief, expected_ext) { + check(brief_view, timestamp - sec_ago * 1000, expected_brief); + check(ext_view, timestamp - sec_ago * 1000, expected_ext); + }; - // Helper to check an absolute TS for an exact match against both views - var checkAbs = function(ts, expected_brief, expected_ext) { - if (!expected_ext) { - expected_ext = expected_brief; - } - check(brief_view, ts, expected_brief); - check(ext_view, ts, expected_ext); - }; + // Helper to check an absolute TS for an exact match against both views + var checkAbs = function(ts, expected_brief, expected_ext) { + if (!expected_ext) { + expected_ext = expected_brief; + } + check(brief_view, ts, expected_brief); + check(ext_view, ts, expected_ext); + }; - // Helper to check an absolute TS for a match at the beginning against - var checkStartsWith = function(view, ts, expected) { - var result = view.getRelativeTimeSpanString(ts); - var regexp = new RegExp("^" + expected); - assert.match(result, regexp); - }; + // Helper to check an absolute TS for a match at the beginning against + var checkStartsWith = function(view, ts, expected) { + var result = view.getRelativeTimeSpanString(ts); + var regexp = new RegExp('^' + expected); + assert.match(result, regexp); + }; - // check integer timestamp, JS Date object and moment object - checkAbs(timestamp, 'now', 'now'); - checkAbs(new Date(), 'now', 'now'); - checkAbs(moment(), 'now', 'now'); + // check integer timestamp, JS Date object and moment object + checkAbs(timestamp, 'now', 'now'); + checkAbs(new Date(), 'now', 'now'); + checkAbs(moment(), 'now', 'now'); - // check recent timestamps - checkDiff(30, 'now', 'now'); // 30 seconds - checkDiff(40*60, '40 minutes', '40 minutes ago'); - checkDiff(60*60, '1 hour', '1 hour ago'); - checkDiff(125*60, '2 hours', '2 hours ago'); + // check recent timestamps + checkDiff(30, 'now', 'now'); // 30 seconds + checkDiff(40 * 60, '40 minutes', '40 minutes ago'); + checkDiff(60 * 60, '1 hour', '1 hour ago'); + checkDiff(125 * 60, '2 hours', '2 hours ago'); - // set to third of month to avoid problems on the 29th/30th/31st - var last_month = moment().subtract(1, 'month').date(3), - months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], - day_of_month = new Date().getDate(); - check(brief_view,last_month, months[last_month.month()] + ' 3'); - checkStartsWith(ext_view,last_month, months[last_month.month()] + ' 3'); + // set to third of month to avoid problems on the 29th/30th/31st + var last_month = moment() + .subtract(1, 'month') + .date(3), + months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ], + day_of_month = new Date().getDate(); + check(brief_view, last_month, months[last_month.month()] + ' 3'); + checkStartsWith(ext_view, last_month, months[last_month.month()] + ' 3'); - // subtract 26 hours to be safe in case of DST stuff - var yesterday = new Date(timestamp - 26*60*60*1000), - days_of_week = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - check(brief_view, yesterday, days_of_week[yesterday.getDay()]); - checkStartsWith(ext_view, yesterday, days_of_week[yesterday.getDay()]); + // subtract 26 hours to be safe in case of DST stuff + var yesterday = new Date(timestamp - 26 * 60 * 60 * 1000), + days_of_week = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + check(brief_view, yesterday, days_of_week[yesterday.getDay()]); + checkStartsWith(ext_view, yesterday, days_of_week[yesterday.getDay()]); - // Check something long ago - // months are zero-indexed in JS for some reason - check(brief_view, new Date(2012, 4, 5, 17, 30, 0), 'May 5, 2012'); - checkStartsWith(ext_view, new Date(2012, 4, 5, 17, 30, 0), 'May 5, 2012'); + // Check something long ago + // months are zero-indexed in JS for some reason + check(brief_view, new Date(2012, 4, 5, 17, 30, 0), 'May 5, 2012'); + checkStartsWith(ext_view, new Date(2012, 4, 5, 17, 30, 0), 'May 5, 2012'); + }); + + describe('updates within a minute reasonable intervals', function() { + var view; + beforeEach(function() { + view = new Whisper.TimestampView(); + }); + afterEach(function() { + clearTimeout(view.timeout); }); - - describe('updates within a minute reasonable intervals', function() { - var view; - beforeEach(function() { - view = new Whisper.TimestampView(); - }); - afterEach(function() { - clearTimeout(view.timeout); - }); - - it('updates timestamps this minute within a minute', function() { - var now = Date.now(); - view.$el.attr('data-timestamp', now - 1000); - view.update(); - assert.isAbove(view.delay, 0); // non zero - assert.isBelow(view.delay, 60 * 1000); // < minute - }); - - it('updates timestamps from this hour within a minute', function() { - var now = Date.now(); - view.$el.attr('data-timestamp', now - 1000 - 1000*60*5); // 5 minutes and 1 sec ago - view.update(); - assert.isAbove(view.delay, 0); // non zero - assert.isBelow(view.delay, 60 * 1000); // minute - }); - - it('updates timestamps from today within an hour', function() { - var now = Date.now(); - view.$el.attr('data-timestamp', now - 1000 - 1000*60*60*5); // 5 hours and 1 sec ago - view.update(); - assert.isAbove(view.delay, 60 * 1000); // minute - assert.isBelow(view.delay, 60 * 60 * 1000); // hour - }); - - it('updates timestamps from this week within a day', function() { - var now = Date.now(); - view.$el.attr('data-timestamp', now - 1000 - 6*24*60*60*1000); // 6 days and 1 sec ago - view.update(); - assert.isAbove(view.delay, 60 * 60 * 1000); // hour - assert.isBelow(view.delay, 24 * 60 * 60 * 1000); // day - }); - - it('does not updates very old timestamps', function() { - var now = Date.now(); - // return falsey value for long ago dates that don't update - view.$el.attr('data-timestamp', now - 8*24*60*60*1000); - view.update(); - assert.notOk(view.delay); - }); - + it('updates timestamps this minute within a minute', function() { + var now = Date.now(); + view.$el.attr('data-timestamp', now - 1000); + view.update(); + assert.isAbove(view.delay, 0); // non zero + assert.isBelow(view.delay, 60 * 1000); // < minute }); + it('updates timestamps from this hour within a minute', function() { + var now = Date.now(); + view.$el.attr('data-timestamp', now - 1000 - 1000 * 60 * 5); // 5 minutes and 1 sec ago + view.update(); + assert.isAbove(view.delay, 0); // non zero + assert.isBelow(view.delay, 60 * 1000); // minute + }); + + it('updates timestamps from today within an hour', function() { + var now = Date.now(); + view.$el.attr('data-timestamp', now - 1000 - 1000 * 60 * 60 * 5); // 5 hours and 1 sec ago + view.update(); + assert.isAbove(view.delay, 60 * 1000); // minute + assert.isBelow(view.delay, 60 * 60 * 1000); // hour + }); + + it('updates timestamps from this week within a day', function() { + var now = Date.now(); + view.$el.attr('data-timestamp', now - 1000 - 6 * 24 * 60 * 60 * 1000); // 6 days and 1 sec ago + view.update(); + assert.isAbove(view.delay, 60 * 60 * 1000); // hour + assert.isBelow(view.delay, 24 * 60 * 60 * 1000); // day + }); + + it('does not updates very old timestamps', function() { + var now = Date.now(); + // return falsey value for long ago dates that don't update + view.$el.attr('data-timestamp', now - 8 * 24 * 60 * 60 * 1000); + view.update(); + assert.notOk(view.delay); + }); + }); }); diff --git a/test/views/whisper_view_test.js b/test/views/whisper_view_test.js index 26296c091..4b2b4396f 100644 --- a/test/views/whisper_view_test.js +++ b/test/views/whisper_view_test.js @@ -3,8 +3,8 @@ describe('Whisper.View', function() { var viewClass = Whisper.View.extend({ template: '
{{ variable }}
', render_attributes: { - variable: 'value' - } + variable: 'value', + }, }); var view = new viewClass(); @@ -13,7 +13,7 @@ describe('Whisper.View', function() { }); it('renders a template with no render_attributes', function() { var viewClass = Whisper.View.extend({ - template: '
static text
' + template: '
static text
', }); var view = new viewClass(); @@ -22,10 +22,12 @@ describe('Whisper.View', function() { }); it('renders a template function with render_attributes function', function() { var viewClass = Whisper.View.extend({ - template: function() { return '
{{ variable }}
'; }, + template: function() { + return '
{{ variable }}
'; + }, render_attributes: function() { return { variable: 'value' }; - } + }, }); var view = new viewClass(); view.render(); diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index eb484e82b..6a9407715 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -1,6 +1,5 @@ import React from 'react'; - /** * A placeholder Message component for now, giving the structure of a plain message with * none of the dynamic functionality. This page will be used to build up our corpus of diff --git a/ts/components/conversation/Quote.tsx b/ts/components/conversation/Quote.tsx index 54365c4f5..b1157202a 100644 --- a/ts/components/conversation/Quote.tsx +++ b/ts/components/conversation/Quote.tsx @@ -4,7 +4,6 @@ import classnames from 'classnames'; import * as MIME from '../../../ts/types/MIME'; import * as GoogleChrome from '../../../ts/util/GoogleChrome'; - interface Props { attachments: Array; authorColor: string; @@ -54,9 +53,9 @@ function getObjectUrl(thumbnail: Attachment | undefined): string | null { export class Quote extends React.Component { public renderImage(url: string, icon?: string) { - const iconElement = icon - ?
- : null; + const iconElement = icon ? ( +
+ ) : null; return (
@@ -98,9 +97,7 @@ export class Quote extends React.Component { : this.renderIcon('movie'); } if (GoogleChrome.isImageTypeSupported(contentType)) { - return objectUrl - ? this.renderImage(objectUrl) - : this.renderIcon('image'); + return objectUrl ? this.renderImage(objectUrl) : this.renderIcon('image'); } if (MIME.isAudio(contentType)) { return this.renderIcon('microphone'); @@ -113,7 +110,9 @@ export class Quote extends React.Component { const { i18n, text, attachments } = this.props; if (text) { - return
; + return ( +
+ ); } if (!attachments || attachments.length === 0) { @@ -140,7 +139,13 @@ export class Quote extends React.Component { } public renderIOSLabel() { - const { i18n, isIncoming, isFromMe, authorTitle, authorProfileName } = this.props; + const { + i18n, + isIncoming, + isFromMe, + authorTitle, + authorProfileName, + } = this.props; const profileString = authorProfileName ? ` ~${authorProfileName}` : ''; const authorName = `${authorTitle}${profileString}`; @@ -185,27 +190,25 @@ export class Quote extends React.Component { isFromMe, } = this.props; - - const authorProfileElement = authorProfileName - ? ~{authorProfileName} - : null; + const authorProfileElement = authorProfileName ? ( + ~{authorProfileName} + ) : null; return (
- { isFromMe - ? i18n('you') - : {authorTitle}{' '}{authorProfileElement} - } + {isFromMe ? ( + i18n('you') + ) : ( + + {authorTitle} {authorProfileElement} + + )}
); } public render() { - const { - authorColor, - onClick, - isFromMe, - } = this.props; + const { authorColor, onClick, isFromMe } = this.props; if (!validateQuote(this.props)) { return null; diff --git a/ts/components/utility/BackboneWrapper.tsx b/ts/components/utility/BackboneWrapper.tsx index a57651ced..0a3e75fe2 100644 --- a/ts/components/utility/BackboneWrapper.tsx +++ b/ts/components/utility/BackboneWrapper.tsx @@ -14,7 +14,7 @@ interface BackboneView { } interface BackboneViewConstructor { - new (options: object): BackboneView; + new (options: object): BackboneView; } /** @@ -41,7 +41,7 @@ export class BackboneWrapper extends React.Component { protected setEl = (element: HTMLDivElement | null) => { this.el = element; this.setup(); - } + }; protected setup = () => { const { el } = this; @@ -56,7 +56,7 @@ export class BackboneWrapper extends React.Component { // It's important to let the view create its own root DOM element. This ensures that // its tagName property actually takes effect. el.appendChild(this.view.el); - } + }; protected teardown() { if (!this.view) { diff --git a/ts/html/index.ts b/ts/html/index.ts index 3aad63218..7121781bc 100644 --- a/ts/html/index.ts +++ b/ts/html/index.ts @@ -1,6 +1,5 @@ import linkTextInternal from '../../js/modules/link_text'; - export const linkText = (value: string): string => linkTextInternal(value, { target: '_blank' }); diff --git a/ts/styleguide/ConversationContext.tsx b/ts/styleguide/ConversationContext.tsx index 139dd8a1e..dba62ce9c 100644 --- a/ts/styleguide/ConversationContext.tsx +++ b/ts/styleguide/ConversationContext.tsx @@ -1,7 +1,6 @@ import React from 'react'; import classnames from 'classnames'; - interface Props { /** * Corresponds to the theme setting in the app, and the class added to the root element. @@ -21,10 +20,8 @@ export class ConversationContext extends React.Component { return (
-
-
    - {this.props.children} -
+
+
    {this.props.children}
diff --git a/ts/styleguide/StyleGuideUtil.ts b/ts/styleguide/StyleGuideUtil.ts index bdf0ab3bf..ecba629d8 100644 --- a/ts/styleguide/StyleGuideUtil.ts +++ b/ts/styleguide/StyleGuideUtil.ts @@ -1,9 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { - padStart, - sample, -} from 'lodash'; +import { padStart, sample } from 'lodash'; import _ from 'lodash'; import moment from 'moment'; @@ -58,7 +55,6 @@ const landscapeRedObjectUrl = makeObjectUrl(landscapeRed, 'image/png'); import portraitTeal from '../../fixtures/50x1000-teal.jpeg'; const portraitTealObjectUrl = makeObjectUrl(portraitTeal, 'image/png'); - function makeObjectUrl(data: ArrayBuffer, contentType: string): string { const blob = new Blob([data], { type: contentType, @@ -69,7 +65,6 @@ function makeObjectUrl(data: ArrayBuffer, contentType: string): string { const ourNumber = '+12025559999'; const groupNumber = '+12025550099'; - export { mp3, mp3ObjectUrl, @@ -93,7 +88,6 @@ export { groupNumber, }; - // Required, or TypeScript complains about adding keys to window const parent = window as any; @@ -112,12 +106,7 @@ import filesize from 'filesize'; const i18n = setup(locale, localeMessages); -export { - theme, - locale, - i18n, -}; - +export { theme, locale, i18n }; parent.i18n = i18n; parent.moment = moment; @@ -145,7 +134,6 @@ parent.filesize = filesize; parent.ConversationController._initialFetchComplete = true; parent.ConversationController._initialPromise = Promise.resolve(); - const COLORS = [ 'red', 'pink', @@ -199,12 +187,7 @@ group.contactCollection.add(CONTACTS[0]); group.contactCollection.add(CONTACTS[1]); group.contactCollection.add(CONTACTS[2]); -export { - COLORS, - CONTACTS, - me, - group, -}; +export { COLORS, CONTACTS, me, group }; parent.textsecure.storage.user.getNumber = () => ourNumber; diff --git a/ts/test/types/Conversation_test.ts b/ts/test/types/Conversation_test.ts index f5aea6fc6..67187f10a 100644 --- a/ts/test/types/Conversation_test.ts +++ b/ts/test/types/Conversation_test.ts @@ -32,10 +32,10 @@ describe('Conversation', () => { currentLastMessageText: 'Existing message', currentTimestamp: 555, lastMessage: { - type: 'outgoing', - conversationId: 'foo', - sent_at: 666, - timestamp: 666, + type: 'outgoing', + conversationId: 'foo', + sent_at: 666, + timestamp: 666, } as OutgoingMessage, lastMessageNotificationText: 'New outgoing message', }; @@ -54,10 +54,10 @@ describe('Conversation', () => { currentLastMessageText: 'bingo', currentTimestamp: 555, lastMessage: { - type: 'verified-change', - conversationId: 'foo', - sent_at: 666, - timestamp: 666, + type: 'verified-change', + conversationId: 'foo', + sent_at: 666, + timestamp: 666, } as VerifiedChangeMessage, lastMessageNotificationText: 'Verified Changed', }; @@ -77,15 +77,15 @@ describe('Conversation', () => { currentLastMessageText: 'I am expired', currentTimestamp: 555, lastMessage: { - type: 'incoming', - conversationId: 'foo', - sent_at: 666, - timestamp: 666, - expirationTimerUpdate: { - expireTimer: 111, - fromSync: false, - source: '+12223334455', - }, + type: 'incoming', + conversationId: 'foo', + sent_at: 666, + timestamp: 666, + expirationTimerUpdate: { + expireTimer: 111, + fromSync: false, + source: '+12223334455', + }, } as IncomingMessage, lastMessageNotificationText: 'Last message before expired', }; diff --git a/ts/test/types/message/initializeAttachmentMetadata_test.ts b/ts/test/types/message/initializeAttachmentMetadata_test.ts index 2979f13f9..d76618f28 100644 --- a/ts/test/types/message/initializeAttachmentMetadata_test.ts +++ b/ts/test/types/message/initializeAttachmentMetadata_test.ts @@ -7,7 +7,6 @@ import { MIMEType } from '../../../../ts/types/MIME'; // @ts-ignore import { stringToArrayBuffer } from '../../../../js/modules/string_to_array_buffer'; - describe('Message', () => { describe('initializeAttachmentMetadata', () => { it('should handle visual media attachments', async () => { @@ -18,12 +17,14 @@ describe('Message', () => { timestamp: 1523317140899, received_at: 1523317140899, sent_at: 1523317140800, - attachments: [{ - contentType: 'image/jpeg' as MIMEType, - data: stringToArrayBuffer('foo'), - fileName: 'foo.jpg', - size: 1111, - }], + attachments: [ + { + contentType: 'image/jpeg' as MIMEType, + data: stringToArrayBuffer('foo'), + fileName: 'foo.jpg', + size: 1111, + }, + ], }; const expected: IncomingMessage = { type: 'incoming', @@ -32,12 +33,14 @@ describe('Message', () => { timestamp: 1523317140899, received_at: 1523317140899, sent_at: 1523317140800, - attachments: [{ - contentType: 'image/jpeg' as MIMEType, - data: stringToArrayBuffer('foo'), - fileName: 'foo.jpg', - size: 1111, - }], + attachments: [ + { + contentType: 'image/jpeg' as MIMEType, + data: stringToArrayBuffer('foo'), + fileName: 'foo.jpg', + size: 1111, + }, + ], hasAttachments: 1, hasVisualMediaAttachments: 1, hasFileAttachments: undefined,