Merge branch 'master' of https://github.com/signalapp/Signal-Desktop into development

# Conflicts:
#	_locales/ar/messages.json
#	_locales/es_419/messages.json
#	_locales/he/messages.json
#	_locales/hi/messages.json
#	_locales/hr/messages.json
#	_locales/kn/messages.json
#	_locales/ko/messages.json
#	_locales/mk/messages.json
#	_locales/sr/messages.json
#	_locales/uk/messages.json
#	_locales/vi/messages.json
#	package.json
pull/6/head
sachaaaaa 7 years ago
commit 33ad509fbf

@ -12,6 +12,7 @@ test/views/*.js
# Generated files
js/components.js
js/libtextsecure.js
js/util_worker.js
js/libsignal-protocol-worker.js
libtextsecure/components.js
libtextsecure/test/test.js

1
.gitignore vendored

@ -17,6 +17,7 @@ sql/
# generated files
js/components.js
js/util_worker.js
js/libtextsecure.js
libtextsecure/components.js
libtextsecure/test/test.js

@ -6,6 +6,7 @@ config/local-*.json
config/local.json
dist/**
js/components.js
js/util_worker.js
js/libtextsecure.js
libtextsecure/components.js
libtextsecure/test/test.js
@ -15,8 +16,10 @@ ts/**/*.js
ts/protobuf/*.d.ts
ts/protobuf/*.js
stylesheets/manifest.css
ts/util/lint/exceptions.json
# Third-party files
node_modules/**
components/**
js/Mp3LameEncoder.min.js
js/WebAudioRecorderMp3.js

@ -9,6 +9,7 @@ install:
script:
- yarn generate
- yarn lint
- yarn lint-deps
- yarn test-node
- yarn nsp check
- yarn prepare-beta-build

@ -7,6 +7,7 @@ const spectron = require('spectron');
const asar = require('asar');
const fs = require('fs');
const assert = require('assert');
const sass = require('node-sass');
/* eslint-disable more/no-then, no-console */
@ -33,6 +34,14 @@ module.exports = grunt => {
src: components,
dest: 'js/components.js',
},
util_worker: {
src: [
'components/bytebuffer/dist/ByteBufferAB.js',
'components/long/dist/Long.js',
'js/util_worker_tasks.js',
],
dest: 'js/util_worker.js',
},
libtextsecurecomponents: {
src: libtextsecurecomponents,
dest: 'libtextsecure/components.js',
@ -90,6 +99,7 @@ module.exports = grunt => {
},
sass: {
options: {
implementation: sass,
sourceMap: true,
importer: importOnce,
},
@ -132,6 +142,9 @@ module.exports = grunt => {
},
},
exec: {
'tx-pull-new': {
cmd: 'tx pull -a --minimum-perc=80',
},
'tx-pull': {
cmd: 'tx pull',
},
@ -414,7 +427,11 @@ module.exports = grunt => {
}
);
grunt.registerTask('tx', ['exec:tx-pull', 'locale-patch']);
grunt.registerTask('tx', [
'exec:tx-pull-new',
'exec:tx-pull',
'locale-patch',
]);
grunt.registerTask('dev', ['default', 'watch']);
grunt.registerTask('test', ['unit-tests', 'lib-unit-tests']);
grunt.registerTask('date', ['gitinfo', 'getExpireTime']);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -549,6 +549,9 @@
"fileSizeWarning": {
"message": "Sorry, the selected file exceeds message size restrictions."
},
"unableToLoadAttachment": {
"message": "Unable to load selected attachment."
},
"disconnected": {
"message": "Disconnected",
"description":
@ -880,15 +883,6 @@
"description":
"Used in alt tag of thumbnail images inside of an embedded message quote"
},
"imageFailedToLoad": {
"message": "Image failed to load",
"description": "When an image attachment is missing, this message is shown"
},
"videoScreenshotFailedToLoad": {
"message": "Video screenshot failed to load",
"description":
"When a attachment video screenshot is missing, this message is shown"
},
"imageAttachmentAlt": {
"message": "Image attached to message",
"description": "Used in alt tag of image attachment"
@ -1228,6 +1222,10 @@
"message": "Unblock this contact to send a message.",
"description": "Brief message shown when trying to message a blocked number"
},
"unblockGroupToSend": {
"message": "Unblock this group to send a message.",
"description": "Brief message shown when trying to message a blocked group"
},
"youChangedTheTimer": {
"message": "You set the disappearing message timer to $time$",
"description":

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -11,8 +11,9 @@ module.exports = {
let initialized = false;
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
async function initialize({ configDir }) {
async function initialize({ configDir, cleanupOrphanedAttachments }) {
if (initialized) {
throw new Error('initialze: Already initialized!');
}
@ -29,8 +30,19 @@ async function initialize({ configDir }) {
event.sender.send(`${ERASE_ATTACHMENTS_KEY}-done`);
} catch (error) {
const errorForDisplay = error && error.stack ? error.stack : error;
console.log(`sql-erase error: ${errorForDisplay}`);
console.log(`erase attachments error: ${errorForDisplay}`);
event.sender.send(`${ERASE_ATTACHMENTS_KEY}-done`, error);
}
});
ipcMain.on(CLEANUP_ORPHANED_ATTACHMENTS_KEY, async event => {
try {
await cleanupOrphanedAttachments();
event.sender.send(`${CLEANUP_ORPHANED_ATTACHMENTS_KEY}-done`);
} catch (error) {
const errorForDisplay = error && error.stack ? error.stack : error;
console.log(`cleanup orphaned attachments error: ${errorForDisplay}`);
event.sender.send(`${CLEANUP_ORPHANED_ATTACHMENTS_KEY}-done`, error);
}
});
}

@ -0,0 +1,58 @@
const fs = require('fs');
const _ = require('lodash');
const ENCODING = 'utf8';
module.exports = {
start,
};
function start(name, targetPath) {
let cachedValue = null;
try {
const text = fs.readFileSync(targetPath, ENCODING);
cachedValue = JSON.parse(text);
console.log(`config/get: Successfully read ${name} config file`);
if (!cachedValue) {
console.log(
`config/get: ${name} config value was falsy, cache is now empty object`
);
cachedValue = Object.create(null);
}
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
console.log(
`config/get: Did not find ${name} config file, cache is now empty object`
);
cachedValue = Object.create(null);
}
function get(keyPath) {
return _.get(cachedValue, keyPath);
}
function set(keyPath, value) {
_.set(cachedValue, keyPath, value);
console.log(`config/set: Saving ${name} config to disk`);
const text = JSON.stringify(cachedValue, null, ' ');
fs.writeFileSync(targetPath, text, ENCODING);
}
function remove() {
console.log(`config/remove: Deleting ${name} config from disk`);
fs.unlinkSync(targetPath);
cachedValue = Object.create(null);
}
return {
set,
get,
remove,
};
}

@ -0,0 +1,12 @@
const path = require('path');
const { app } = require('electron');
const { start } = require('./base_config');
const userDataPath = app.getPath('userData');
const targetPath = path.join(userDataPath, 'ephemeral.json');
const ephemeralConfig = start('ephemeral', targetPath);
module.exports = ephemeralConfig;

@ -1,4 +1,5 @@
const path = require('path');
const fs = require('fs');
function _eliminateAllAfterCharacter(string, character) {
const index = string.indexOf(character);
@ -24,21 +25,27 @@ function _createFileHandler({ userDataPath, installPath, isWindows }) {
return (request, callback) => {
// normalize() is primarily useful here for switching / to \ on windows
const target = path.normalize(_urlToPath(request.url, { isWindows }));
const realPath = fs.existsSync(target) ? fs.realpathSync(target) : target;
if (!path.isAbsolute(target)) {
console.log(`Warning: denying request to non-absolute path '${target}'`);
if (!path.isAbsolute(realPath)) {
console.log(
`Warning: denying request to non-absolute path '${realPath}'`
);
return callback();
}
if (!target.startsWith(userDataPath) && !target.startsWith(installPath)) {
if (
!realPath.startsWith(userDataPath) &&
!realPath.startsWith(installPath)
) {
console.log(
`Warning: denying request to path '${target}' (userDataPath: '${userDataPath}', installPath: '${installPath}')`
`Warning: denying request to path '${realPath}' (userDataPath: '${userDataPath}', installPath: '${installPath}')`
);
return callback();
}
return callback({
path: target,
path: realPath,
});
};
}

@ -1,5 +1,7 @@
const electron = require('electron');
const sql = require('./sql');
const { remove: removeUserConfig } = require('./user_config');
const { remove: removeEphemeralConfig } = require('./ephemeral_config');
const { ipcMain } = electron;
@ -12,16 +14,12 @@ let initialized = false;
const SQL_CHANNEL_KEY = 'sql-channel';
const ERASE_SQL_KEY = 'erase-sql-key';
function initialize({ userConfig }) {
function initialize() {
if (initialized) {
throw new Error('sqlChannels: already initialized!');
}
initialized = true;
if (!userConfig) {
throw new Error('initialize: userConfig is required!');
}
ipcMain.on(SQL_CHANNEL_KEY, async (event, jobId, callName, ...args) => {
try {
const fn = sql[callName];
@ -44,7 +42,8 @@ function initialize({ userConfig }) {
ipcMain.on(ERASE_SQL_KEY, async event => {
try {
userConfig.set('key', null);
removeUserConfig();
removeEphemeralConfig();
event.sender.send(`${ERASE_SQL_KEY}-done`);
} catch (error) {
const errorForDisplay = error && error.stack ? error.stack : error;

@ -1,11 +1,11 @@
const path = require('path');
const { app } = require('electron');
const ElectronConfig = require('electron-config');
const { start } = require('./base_config');
const config = require('./config');
// use a separate data directory for development
// Use separate data directory for development
if (config.has('storageProfile')) {
const userData = path.join(
app.getPath('appData'),
@ -17,7 +17,9 @@ if (config.has('storageProfile')) {
console.log(`userData: ${app.getPath('userData')}`);
// this needs to be below our update to the appData path
const userConfig = new ElectronConfig();
const userDataPath = app.getPath('userData');
const targetPath = path.join(userDataPath, 'config.json');
const userConfig = start('user', targetPath);
module.exports = userConfig;

@ -14,6 +14,7 @@ install:
build_script:
- yarn generate
- yarn lint-windows
- yarn lint-deps
- yarn test-node
- yarn nsp check
- node build\grunt.js

@ -408,6 +408,10 @@
);
window.log.info('Cleanup: complete');
if (newVersion) {
await window.Signal.Data.cleanupOrphanedAttachments();
}
Views.Initialization.setMessage(window.i18n('loading'));
// Note: We are not invoking the second set of IndexedDB migrations because it is
@ -798,6 +802,7 @@
}
function onConfiguration(ev) {
storage.put('read-receipt-setting', ev.configuration.readReceipts);
ev.confirm();
}
async function onContactReceived(ev) {
@ -915,6 +920,12 @@
updates.left = true;
}
if (details.blocked) {
storage.addBlockedGroup(id);
} else {
storage.removeBlockedGroup(id);
}
await wrapDeferred(conversation.save(updates));
const { expireTimer } = details;
const isValidExpireTimer = typeof expireTimer === 'number';
@ -1105,9 +1116,10 @@
conversationId: data.destination,
type: 'outgoing',
sent: true,
expirationStartTimestamp: data.expirationStartTimestamp
? Math.min(data.expirationStartTimestamp, Date.now())
: null,
expirationStartTimestamp: Math.min(
data.expirationStartTimestamp || data.timestamp || Date.now(),
Date.now()
),
});
}

@ -4,27 +4,54 @@
(function() {
'use strict';
const BLOCKED_NUMBERS_ID = 'blocked';
const BLOCKED_GROUPS_ID = 'blocked-groups';
storage.isBlocked = number => {
const numbers = storage.get('blocked', []);
const numbers = storage.get(BLOCKED_NUMBERS_ID, []);
return _.include(numbers, number);
};
storage.addBlockedNumber = number => {
const numbers = storage.get('blocked', []);
const numbers = storage.get(BLOCKED_NUMBERS_ID, []);
if (_.include(numbers, number)) {
return;
}
window.log.info('adding', number, 'to blocked list');
storage.put('blocked', numbers.concat(number));
storage.put(BLOCKED_NUMBERS_ID, numbers.concat(number));
};
storage.removeBlockedNumber = number => {
const numbers = storage.get('blocked', []);
const numbers = storage.get(BLOCKED_NUMBERS_ID, []);
if (!_.include(numbers, number)) {
return;
}
window.log.info('removing', number, 'from blocked list');
storage.put('blocked', _.without(numbers, number));
storage.put(BLOCKED_NUMBERS_ID, _.without(numbers, number));
};
storage.isGroupBlocked = groupId => {
const groupIds = storage.get(BLOCKED_GROUPS_ID, []);
return _.include(groupIds, groupId);
};
storage.addBlockedGroup = groupId => {
const groupIds = storage.get(BLOCKED_GROUPS_ID, []);
if (_.include(groupIds, groupId)) {
return;
}
window.log.info(`adding groupId(${groupId}) to blocked list`);
storage.put(BLOCKED_GROUPS_ID, groupIds.concat(groupId));
};
storage.removeBlockedGroup = groupId => {
const groupIds = storage.get(BLOCKED_GROUPS_ID, []);
if (!_.include(groupIds, groupId)) {
return;
}
window.log.info(`removing group(${groupId} from blocked list`);
storage.put(BLOCKED_GROUPS_ID, _.without(groupIds, groupId));
};
})();

@ -166,15 +166,18 @@
this.updateLastMessage();
const removeMessage = () => {
const existing = this.messageCollection.get(message.id);
const { id } = message;
const existing = this.messageCollection.get(id);
if (!existing) {
return;
}
window.log.info('Remove expired message from collection', {
sentAt: message.get('sent_at'),
sentAt: existing.get('sent_at'),
});
this.messageCollection.remove(message.id);
this.messageCollection.remove(id);
existing.trigger('expired');
};
// If a fetch is in progress, then we need to wait until that's complete to
@ -778,11 +781,12 @@
});
const message = this.addSingleMessage(messageWithSchema);
this.lastMessage = message.getNotificationText();
this.lastMessageStatus = 'sending';
this.save({
active_at: now,
timestamp: now,
lastMessage: message.getNotificationText(),
lastMessageStatus: 'sending',
});
if (this.isPrivate()) {

@ -83,6 +83,7 @@
this.on('change:expirationStartTimestamp', this.setToExpire);
this.on('change:expireTimer', this.setToExpire);
this.on('unload', this.unload);
this.on('expired', this.onExpired);
this.setToExpire();
},
idForLogging() {
@ -233,7 +234,9 @@
this.quotedMessage = null;
}
},
onExpired() {
this.hasExpired = true;
},
getPropsForTimerNotification() {
const { expireTimer, fromSync, source } = this.get(
'expirationTimerUpdate'
@ -424,6 +427,7 @@
attachment: this.getPropsForAttachment(firstAttachment),
quote: this.getPropsForQuote(),
authorAvatarPath,
isExpired: this.hasExpired,
expirationLength,
expirationTimestamp,
onReply: () => this.trigger('reply', this),
@ -875,17 +879,33 @@
promises.push(c.getProfiles());
}
} else {
this.saveErrors(result.errors);
if (result.successfulNumbers.length > 0) {
const sentTo = this.get('sent_to') || [];
// Note: In a partially-successful group send, we do not start
// the expiration timer.
// In groups, we don't treat unregistered users as a user-visible
// error. The message will look successful, but the details
// screen will show that we didn't send to these unregistered users.
const filteredErrors = _.reject(
result.errors,
error => error.name === 'UnregisteredUserError'
);
// We don't start the expiration timer if there are real errors
// left after filtering out all of the unregistered user errors.
const expirationStartTimestamp = filteredErrors.length
? null
: Date.now();
this.saveErrors(filteredErrors);
this.set({
sent_to: _.union(sentTo, result.successfulNumbers),
sent: true,
expirationStartTimestamp,
});
promises.push(this.sendSyncMessage());
} else {
this.saveErrors(result.errors);
}
promises = promises.concat(
_.map(result.errors, error => {
@ -1185,8 +1205,8 @@
!conversationTimestamp ||
message.get('sent_at') > conversationTimestamp
) {
conversation.lastMessage = message.getNotificationText();
conversation.set({
lastMessage: message.getNotificationText(),
timestamp: message.get('sent_at'),
});
}

@ -909,7 +909,7 @@ function exportConversations(db, options) {
});
}
function getDirectory(options) {
function getDirectory(options = {}) {
return new Promise((resolve, reject) => {
const browserWindow = BrowserWindow.getFocusedWindow();
const dialogOptions = {
@ -1199,11 +1199,7 @@ function assembleLookup(db, storeName, keyFunction) {
}
function getDirectoryForExport() {
const options = {
title: i18n('exportChooserTitle'),
buttonLabel: i18n('exportButton'),
};
return getDirectory(options);
return getDirectory();
}
function createZip(zipDir, targetDir) {
@ -1324,7 +1320,6 @@ async function exportToDirectory(directory, options) {
function getDirectoryForImport() {
const options = {
title: i18n('importChooserTitle'),
buttonLabel: i18n('importButton'),
};
return getDirectory(options);
}

@ -22,6 +22,7 @@ const DATABASE_UPDATE_TIMEOUT = 2 * 60 * 1000; // two minutes
const SQL_CHANNEL_KEY = 'sql-channel';
const ERASE_SQL_KEY = 'erase-sql-key';
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
const _jobs = Object.create(null);
const _DEBUG = false;
@ -64,6 +65,7 @@ module.exports = {
removeAll,
removeOtherData,
cleanupOrphanedAttachments,
// Returning plain JSON
getMessagesNeedingUpgrade,
@ -164,7 +166,7 @@ ipcRenderer.on(
const job = _getJob(jobId);
if (!job) {
throw new Error(
`Received job reply to job ${jobId}, but did not have it in our registry!`
`Received SQL channel reply to job ${jobId}, but did not have it in our registry!`
);
}
@ -172,7 +174,9 @@ ipcRenderer.on(
if (errorForDisplay) {
return reject(
new Error(`Error calling channel ${fnName}: ${errorForDisplay}`)
new Error(
`Error received from SQL channel job ${jobId} (${fnName}): ${errorForDisplay}`
)
);
}
@ -194,7 +198,8 @@ function makeChannel(fnName) {
});
setTimeout(
() => reject(new Error(`Request to ${fnName} timed out`)),
() =>
reject(new Error(`SQL channel job ${jobId} (${fnName}) timed out`)),
DATABASE_UPDATE_TIMEOUT
);
});
@ -244,8 +249,7 @@ async function removeMessage(id, { Message }) {
// it needs to delete all associated on-disk files along with the database delete.
if (message) {
await channels.removeMessage(id);
const model = new Message(message);
await model.cleanup();
await message.cleanup();
}
}
@ -388,6 +392,10 @@ async function removeAll() {
await channels.removeAll();
}
async function cleanupOrphanedAttachments() {
await callChannel(CLEANUP_ORPHANED_ATTACHMENTS_KEY);
}
// Note: will need to restart the app after calling this, to set up afresh
async function removeOtherData() {
await Promise.all([

@ -1,6 +1,6 @@
/* global window, IDBKeyRange */
const { includes, isFunction, isString, last, forEach } = require('lodash');
const { includes, isFunction, isString, last, map } = require('lodash');
const {
saveMessages,
_removeMessages,
@ -83,23 +83,25 @@ async function migrateToSQL({
const status = await migrateStoreToSQLite({
db,
save: async array => {
forEach(array, item => {
// In the new database, we can't store ArrayBuffers, so we turn these two fields
// into strings like MessageReceiver now does before save.
await Promise.all(
map(array, async item => {
// In the new database, we can't store ArrayBuffers, so we turn these two
// fields into strings like MessageReceiver now does before save.
// Need to set it to version two, since we're using Base64 strings now
// eslint-disable-next-line no-param-reassign
item.version = 2;
if (item.envelope) {
// eslint-disable-next-line no-param-reassign
item.envelope = arrayBufferToString(item.envelope);
}
if (item.decrypted) {
// Need to set it to version two, since we're using Base64 strings now
// eslint-disable-next-line no-param-reassign
item.decrypted = arrayBufferToString(item.decrypted);
}
});
item.version = 2;
if (item.envelope) {
// eslint-disable-next-line no-param-reassign
item.envelope = await arrayBufferToString(item.envelope);
}
if (item.decrypted) {
// eslint-disable-next-line no-param-reassign
item.decrypted = await arrayBufferToString(item.decrypted);
}
})
);
await saveUnprocesseds(array);
},
remove: removeUnprocessed,

@ -0,0 +1,44 @@
/* global dcodeIO */
/* eslint-disable strict */
'use strict';
const functions = {
stringToArrayBufferBase64,
arrayBufferToStringBase64,
};
onmessage = async e => {
const [jobId, fnName, ...args] = e.data;
try {
const fn = functions[fnName];
if (!fn) {
throw new Error(`Worker: job ${jobId} did not find function ${fnName}`);
}
const result = await fn(...args);
postMessage([jobId, null, result]);
} catch (error) {
const errorForDisplay = prepareErrorForPostMessage(error);
postMessage([jobId, errorForDisplay]);
}
};
function prepareErrorForPostMessage(error) {
if (!error) {
return null;
}
if (error.stack) {
return error.stack;
}
return error.message;
}
function stringToArrayBufferBase64(string) {
return dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();
}
function arrayBufferToStringBase64(arrayBuffer) {
return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
}

@ -34,11 +34,15 @@
this.render();
try {
await Database.clear();
await Database.close();
window.log.info('All database connections closed. Starting delete.');
window.log.info(
'All database connections closed. Starting database drop.'
);
await Database.drop();
} catch (error) {
window.log.error(
'Something went wrong closing all database connections.'
'Something went wrong deleting IndexedDB data then dropping database.'
);
}
@ -46,15 +50,14 @@
},
async clearAllData() {
try {
await Promise.all([
Logs.deleteAll(),
Database.drop(),
window.Signal.Data.removeAll(),
window.Signal.Data.removeOtherData(),
]);
await Logs.deleteAll();
// SQLCipher
await window.Signal.Data.removeAll();
await window.Signal.Data.close();
await window.Signal.Data.removeDB();
await window.Signal.Data.removeOtherData();
} catch (error) {
window.log.error(
'Something went wrong deleting all data:',

@ -29,6 +29,11 @@
return { toastMessage: i18n('unblockToSend') };
},
});
Whisper.BlockedGroupToast = Whisper.ToastView.extend({
render_attributes() {
return { toastMessage: i18n('unblockGroupToSend') };
},
});
Whisper.LeftGroupToast = Whisper.ToastView.extend({
render_attributes() {
return { toastMessage: i18n('youLeftTheGroup') };
@ -1436,6 +1441,9 @@
if (this.model.isPrivate() && storage.isBlocked(this.model.id)) {
toast = new Whisper.BlockedToast();
}
if (!this.model.isPrivate() && storage.isGroupBlocked(this.model.id)) {
toast = new Whisper.BlockedGroupToast();
}
if (!this.model.isPrivate() && this.model.get('left')) {
toast = new Whisper.LeftGroupToast();
}

@ -24,6 +24,12 @@
};
},
});
Whisper.UnableToLoadToast = Whisper.ToastView.extend({
render_attributes() {
return { toastMessage: i18n('unableToLoadAttachment') };
},
});
Whisper.UnsupportedFileTypeToast = Whisper.ToastView.extend({
template: i18n('unsupportedFileType'),
});
@ -88,14 +94,21 @@
this.thumb.$('img')[0].onload = () => {
this.$el.trigger('force-resize');
};
this.thumb.$('img')[0].onerror = () => {
this.unableToLoadAttachment();
};
},
unableToLoadAttachment() {
const toast = new Whisper.UnableToLoadToast();
toast.$el.insertAfter(this.$el);
toast.render();
this.deleteFiles();
},
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/tiff') {
// nothing to do
return Promise.resolve(file);
}
@ -111,14 +124,19 @@
const maxHeight = 4096;
const maxWidth = 4096;
if (
img.width <= maxWidth &&
img.height <= maxHeight &&
img.naturalWidth <= maxWidth &&
img.naturalHeight <= maxHeight &&
file.size <= maxSize
) {
resolve(file);
return;
}
if (file.type === 'image/gif') {
reject(new Error('GIF is too large'));
return;
}
const canvas = loadImage.scale(img, {
canvas: true,
maxWidth,
@ -180,6 +198,9 @@
const renderImagePreview = async () => {
if (!MIME.isJPEG(file.type)) {
this.previewObjectUrl = URL.createObjectURL(file);
if (!this.previewObjectUrl) {
throw new Error('Failed to create object url for image!');
}
this.addThumb(this.previewObjectUrl);
return;
}
@ -206,42 +227,51 @@
this.addThumb('images/file.svg');
}
const blob = await this.autoScale(file);
let limitKb = 1000000;
const blobType =
file.type === 'image/gif' ? 'gif' : contentType.split('/')[0];
switch (blobType) {
case 'image':
limitKb = 6000;
break;
case 'gif':
limitKb = 25000;
break;
case 'audio':
limitKb = 100000;
break;
case 'video':
limitKb = 100000;
break;
default:
limitKb = 100000;
break;
}
if ((blob.size / 1024).toFixed(4) >= limitKb) {
const units = ['kB', 'MB', 'GB'];
let u = -1;
let limit = limitKb * 1000;
do {
limit /= 1000;
u += 1;
} while (limit >= 1000 && u < units.length - 1);
const toast = new Whisper.FileSizeToast({
model: { limit, units: units[u] },
});
toast.$el.insertAfter(this.$el);
toast.render();
this.deleteFiles();
try {
const blob = await this.autoScale(file);
let limitKb = 1000000;
const blobType =
file.type === 'image/gif' ? 'gif' : contentType.split('/')[0];
switch (blobType) {
case 'image':
limitKb = 6000;
break;
case 'gif':
limitKb = 25000;
break;
case 'audio':
limitKb = 100000;
break;
case 'video':
limitKb = 100000;
break;
default:
limitKb = 100000;
break;
}
if ((blob.size / 1024).toFixed(4) >= limitKb) {
const units = ['kB', 'MB', 'GB'];
let u = -1;
let limit = limitKb * 1000;
do {
limit /= 1000;
u += 1;
} while (limit >= 1000 && u < units.length - 1);
const toast = new Whisper.FileSizeToast({
model: { limit, units: units[u] },
});
toast.$el.insertAfter(this.$el);
toast.render();
this.deleteFiles();
}
} catch (error) {
window.log.error(
'Error ensuring that image is properly sized:',
error && error.message ? error.message : error
);
this.unableToLoadAttachment();
}
},

@ -15,6 +15,7 @@
this.listenTo(this.model, 'change', this.onChange);
this.listenTo(this.model, 'destroy', this.onDestroy);
this.listenTo(this.model, 'unload', this.onUnload);
this.listenTo(this.model, 'expired', this.onExpired);
},
onChange() {
this.addId();
@ -27,6 +28,9 @@
const { id } = this.model;
this.$el.attr('id', id);
},
onExpired() {
setTimeout(() => this.onUnload(), 1000);
},
onUnload() {
if (this.childView) {
this.childView.remove();
@ -93,18 +97,26 @@
};
this.listenTo(this.model, 'change', update);
this.listenTo(this.model, 'expired', update);
const applicableConversationChanges =
'change:color change:name change:number change:profileName change:profileAvatar';
this.conversation = this.model.getConversation();
this.listenTo(this.conversation, 'change', update);
this.listenTo(this.conversation, applicableConversationChanges, update);
this.fromContact = this.model.getIncomingContact();
if (this.fromContact) {
this.listenTo(this.fromContact, 'change', update);
this.listenTo(this.fromContact, applicableConversationChanges, update);
}
this.quotedContact = this.model.getQuoteContact();
if (this.quotedContact) {
this.listenTo(this.quotedContact, 'change', update);
this.listenTo(
this.quotedContact,
applicableConversationChanges,
update
);
}
this.$el.append(this.childView.el);

@ -9,9 +9,112 @@
/* global _: false */
/* global ContactBuffer: false */
/* global GroupBuffer: false */
/* global Worker: false */
/* eslint-disable more/no-then */
const WORKER_TIMEOUT = 60 * 1000; // one minute
const _utilWorker = new Worker('js/util_worker.js');
const _jobs = Object.create(null);
const _DEBUG = false;
let _jobCounter = 0;
function _makeJob(fnName) {
_jobCounter += 1;
const id = _jobCounter;
if (_DEBUG) {
window.log.info(`Worker job ${id} (${fnName}) started`);
}
_jobs[id] = {
fnName,
start: Date.now(),
};
return id;
}
function _updateJob(id, data) {
const { resolve, reject } = data;
const { fnName, start } = _jobs[id];
_jobs[id] = {
..._jobs[id],
...data,
resolve: value => {
_removeJob(id);
const end = Date.now();
window.log.info(
`Worker job ${id} (${fnName}) succeeded in ${end - start}ms`
);
return resolve(value);
},
reject: error => {
_removeJob(id);
const end = Date.now();
window.log.info(
`Worker job ${id} (${fnName}) failed in ${end - start}ms`
);
return reject(error);
},
};
}
function _removeJob(id) {
if (_DEBUG) {
_jobs[id].complete = true;
} else {
delete _jobs[id];
}
}
function _getJob(id) {
return _jobs[id];
}
async function callWorker(fnName, ...args) {
const jobId = _makeJob(fnName);
return new Promise((resolve, reject) => {
_utilWorker.postMessage([jobId, fnName, ...args]);
_updateJob(jobId, {
resolve,
reject,
args: _DEBUG ? args : null,
});
setTimeout(
() => reject(new Error(`Worker job ${jobId} (${fnName}) timed out`)),
WORKER_TIMEOUT
);
});
}
_utilWorker.onmessage = e => {
const [jobId, errorForDisplay, result] = e.data;
const job = _getJob(jobId);
if (!job) {
throw new Error(
`Received worker reply to job ${jobId}, but did not have it in our registry!`
);
}
const { resolve, reject, fnName } = job;
if (errorForDisplay) {
return reject(
new Error(
`Error received from worker job ${jobId} (${fnName}): ${errorForDisplay}`
)
);
}
return resolve(result);
};
function MessageReceiver(username, password, signalingKey, options = {}) {
this.count = 0;
@ -32,13 +135,14 @@ function MessageReceiver(username, password, signalingKey, options = {}) {
}
MessageReceiver.stringToArrayBuffer = string =>
dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer();
Promise.resolve(dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer());
MessageReceiver.arrayBufferToString = arrayBuffer =>
dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary');
Promise.resolve(dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary'));
MessageReceiver.stringToArrayBufferBase64 = string =>
dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();
callWorker('stringToArrayBufferBase64', string);
MessageReceiver.arrayBufferToStringBase64 = arrayBuffer =>
dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
callWorker('arrayBufferToStringBase64', arrayBuffer);
MessageReceiver.prototype = new textsecure.EventTarget();
MessageReceiver.prototype.extend({
@ -273,13 +377,8 @@ MessageReceiver.prototype.extend({
async queueAllCached() {
const items = await this.getAllFromCache();
for (let i = 0, max = items.length; i < max; i += 1) {
if (i > 0 && i % 20 === 0) {
window.log.info('queueAllCached: Giving event loop a rest');
// eslint-disable-next-line no-await-in-loop
await new Promise(resolve => setTimeout(resolve, 2000));
}
this.queueCached(items[i]);
// eslint-disable-next-line no-await-in-loop
await this.queueCached(items[i]);
}
},
async queueCached(item) {
@ -287,13 +386,13 @@ MessageReceiver.prototype.extend({
let envelopePlaintext = item.envelope;
if (item.version === 2) {
envelopePlaintext = MessageReceiver.stringToArrayBufferBase64(
envelopePlaintext = await MessageReceiver.stringToArrayBufferBase64(
envelopePlaintext
);
}
if (typeof envelopePlaintext === 'string') {
envelopePlaintext = MessageReceiver.stringToArrayBuffer(
envelopePlaintext = await MessageReceiver.stringToArrayBuffer(
envelopePlaintext
);
}
@ -304,13 +403,13 @@ MessageReceiver.prototype.extend({
let payloadPlaintext = decrypted;
if (item.version === 2) {
payloadPlaintext = MessageReceiver.stringToArrayBufferBase64(
payloadPlaintext = await MessageReceiver.stringToArrayBufferBase64(
payloadPlaintext
);
}
if (typeof payloadPlaintext === 'string') {
payloadPlaintext = MessageReceiver.stringToArrayBuffer(
payloadPlaintext = await MessageReceiver.stringToArrayBuffer(
payloadPlaintext
);
}
@ -377,12 +476,12 @@ MessageReceiver.prototype.extend({
);
});
},
addToCache(envelope, plaintext) {
async addToCache(envelope, plaintext) {
const id = this.getEnvelopeId(envelope);
const data = {
id,
version: 2,
envelope: MessageReceiver.arrayBufferToStringBase64(plaintext),
envelope: await MessageReceiver.arrayBufferToStringBase64(plaintext),
timestamp: Date.now(),
attempts: 1,
};
@ -401,10 +500,13 @@ MessageReceiver.prototype.extend({
if (item.get('version') === 2) {
item.set(
'decrypted',
MessageReceiver.arrayBufferToStringBase64(plaintext)
await MessageReceiver.arrayBufferToStringBase64(plaintext)
);
} else {
item.set('decrypted', MessageReceiver.arrayBufferToString(plaintext));
item.set(
'decrypted',
await MessageReceiver.arrayBufferToString(plaintext)
);
}
return textsecure.storage.unprocessed.save(item.attributes);
@ -627,6 +729,23 @@ MessageReceiver.prototype.extend({
}
return p.then(() =>
this.processDecrypted(envelope, msg, this.number).then(message => {
const groupId = message.group && message.group.id;
const isBlocked = this.isGroupBlocked(groupId);
const isMe = envelope.source === textsecure.storage.user.getNumber();
const isLeavingGroup = Boolean(
message.group &&
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT
);
if (groupId && isBlocked && !(isMe && isLeavingGroup)) {
window.log.warn(
`Message ${this.getEnvelopeId(
envelope
)} ignored; destined for blocked group`
);
return this.removeFromCache(envelope);
}
const ev = new Event('sent');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = {
@ -651,6 +770,23 @@ MessageReceiver.prototype.extend({
}
return p.then(() =>
this.processDecrypted(envelope, msg, envelope.source).then(message => {
const groupId = message.group && message.group.id;
const isBlocked = this.isGroupBlocked(groupId);
const isMe = envelope.source === textsecure.storage.user.getNumber();
const isLeavingGroup = Boolean(
message.group &&
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT
);
if (groupId && isBlocked && !(isMe && isLeavingGroup)) {
window.log.warn(
`Message ${this.getEnvelopeId(
envelope
)} ignored; destined for blocked group`
);
return this.removeFromCache(envelope);
}
const ev = new Event('message');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = {
@ -891,10 +1027,22 @@ MessageReceiver.prototype.extend({
handleBlocked(envelope, blocked) {
window.log.info('Setting these numbers as blocked:', blocked.numbers);
textsecure.storage.put('blocked', blocked.numbers);
const groupIds = _.map(blocked.groupIds, groupId => groupId.toBinary());
window.log.info(
'Setting these groups as blocked:',
groupIds.map(groupId => `group(${groupId})`)
);
textsecure.storage.put('blocked-groups', groupIds);
return this.removeFromCache(envelope);
},
isBlocked(number) {
return textsecure.storage.get('blocked', []).indexOf(number) >= 0;
},
isGroupBlocked(groupId) {
return textsecure.storage.get('blocked-groups', []).indexOf(groupId) >= 0;
},
handleAttachment(attachment) {
// eslint-disable-next-line no-param-reassign
attachment.id = attachment.id.toString();

@ -10,8 +10,12 @@ const _ = require('lodash');
const pify = require('pify');
const electron = require('electron');
const getRealPath = pify(fs.realpath);
const packageJson = require('./package.json');
const GlobalErrors = require('./app/global_errors');
GlobalErrors.addHandler();
const getRealPath = pify(fs.realpath);
const {
app,
BrowserWindow,
@ -22,26 +26,6 @@ const {
shell,
} = electron;
const packageJson = require('./package.json');
const sql = require('./app/sql');
const sqlChannels = require('./app/sql_channel');
// const attachments = require('./app/attachments');
const attachmentChannel = require('./app/attachment_channel');
const autoUpdate = require('./app/auto_update');
const createTrayIcon = require('./app/tray_icon');
const GlobalErrors = require('./app/global_errors');
const logging = require('./app/logging');
const windowState = require('./app/window_state');
const { createTemplate } = require('./app/menu');
const {
installFileHandler,
installWebHandler,
} = require('./app/protocol_filter');
const { installPermissionsHandler } = require('./app/permissions');
GlobalErrors.addHandler();
const appUserModelId = `org.whispersystems.${packageJson.name}`;
console.log('Set Windows Application User Model ID (AUMID)', {
appUserModelId,
@ -64,14 +48,32 @@ const usingTrayIcon =
const config = require('./app/config');
// Very important to put before the single instance check, since it is based on the
// userData directory.
const userConfig = require('./app/user_config');
const importMode =
process.argv.some(arg => arg === '--import') || config.get('import');
const development = config.environment === 'development';
// Very important to put before the single instance check, since it is based on the
// userData directory.
const userConfig = require('./app/user_config');
// We generally want to pull in our own modules after this point, after the user
// data directory has been set.
const attachments = require('./app/attachments');
const attachmentChannel = require('./app/attachment_channel');
const autoUpdate = require('./app/auto_update');
const createTrayIcon = require('./app/tray_icon');
const ephemeralConfig = require('./app/ephemeral_config');
const logging = require('./app/logging');
const sql = require('./app/sql');
const sqlChannels = require('./app/sql_channel');
const windowState = require('./app/window_state');
const { createTemplate } = require('./app/menu');
const {
installFileHandler,
installWebHandler,
} = require('./app/protocol_filter');
const { installPermissionsHandler } = require('./app/permissions');
function showWindow() {
if (!mainWindow) {
@ -114,7 +116,14 @@ if (!process.mas) {
}
}
let windowConfig = userConfig.get('window');
const windowFromUserConfig = userConfig.get('window');
const windowFromEphemeral = ephemeralConfig.get('window');
let windowConfig = windowFromEphemeral || windowFromUserConfig;
if (windowFromUserConfig) {
userConfig.set('window', null);
ephemeralConfig.set('window', windowConfig);
}
const loadLocale = require('./app/locale').load;
// Both of these will be set after app fires the 'ready' event
@ -284,7 +293,7 @@ function createWindow() {
'Updating BrowserWindow config: %s',
JSON.stringify(windowConfig)
);
userConfig.set('window', windowConfig);
ephemeralConfig.set('window', windowConfig);
}
const debouncedCaptureStats = _.debounce(captureAndSaveWindowStats, 500);
@ -618,24 +627,33 @@ app.on('ready', async () => {
locale = loadLocale({ appLocale, logger });
}
await attachmentChannel.initialize({ configDir: userDataPath });
let key = userConfig.get('key');
if (!key) {
console.log(
'key/initialize: Generating new encryption key, since we did not find it on disk'
);
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
key = crypto.randomBytes(32).toString('hex');
userConfig.set('key', key);
}
await sql.initialize({ configDir: userDataPath, key });
await sqlChannels.initialize({ userConfig });
// const allAttachments = await attachments.getAllAttachments(userDataPath);
// const orphanedAttachments = await sql.removeKnownAttachments(allAttachments);
// await attachments.deleteAll({
// userDataPath,
// attachments: orphanedAttachments,
// });
await sqlChannels.initialize();
async function cleanupOrphanedAttachments() {
const allAttachments = await attachments.getAllAttachments(userDataPath);
const orphanedAttachments = await sql.removeKnownAttachments(
allAttachments
);
await attachments.deleteAll({
userDataPath,
attachments: orphanedAttachments,
});
}
await attachmentChannel.initialize({
configDir: userDataPath,
cleanupOrphanedAttachments,
});
ready = true;

@ -3,7 +3,7 @@
"productName": "Loki Messenger",
"description": "Private messaging from your desktop",
"repository": "https://github.com/sloki-project/loki-messenger.git",
"version": "1.15.4-beta.1",
"version": "1.16.2",
"license": "GPL-3.0",
"author": {
"name": "Open Whisper Systems",
@ -32,6 +32,7 @@
"eslint": "eslint .",
"lint": "yarn format --list-different && yarn lint-windows",
"lint-windows": "yarn eslint && yarn tslint",
"lint-deps": "node ts/util/lint/linter.js",
"tslint": "tslint --format stylish --project .",
"format": "prettier --write \"*.{css,js,json,md,scss,ts,tsx}\" \"./**/*.{css,js,json,md,scss,ts,tsx}\"",
"transpile": "tsc",
@ -40,6 +41,7 @@
"styleguide": "styleguidist server"
},
"dependencies": {
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#ed4f4d179ac010c6347b291cbd4c2ebe5c773741",
"@sindresorhus/is": "^0.8.0",
"archiver": "^2.1.1",
"backbone": "^1.3.3",
@ -49,10 +51,8 @@
"bunyan": "^1.8.12",
"classnames": "^2.2.5",
"config": "^1.28.1",
"electron-config": "^1.0.0",
"electron-editor-context-menu": "^1.1.1",
"electron-is-dev": "^0.3.0",
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#ed4f4d179ac010c6347b291cbd4c2ebe5c773741",
"electron-unhandled": "https://github.com/scottnonnenberg-signal/electron-unhandled.git#7496187472aa561d39fcd4c843a54ffbef0a388c",
"electron-updater": "^2.21.10",
"emoji-datasource": "4.0.0",
@ -74,9 +74,11 @@
"moment": "^2.21.0",
"mustache": "^2.3.0",
"node-fetch": "https://github.com/scottnonnenberg-signal/node-fetch.git#3e5f51e08c647ee5f20c43b15cf2d352d61c36b4",
"node-gyp": "^3.8.0",
"node-sass": "^4.9.3",
"os-locale": "^2.1.0",
"pify": "^3.0.0",
"protobufjs": "^6.8.6",
"protobufjs": "~6.8.6",
"proxy-agent": "^2.1.0",
"react": "^16.2.0",
"react-contextmenu": "^2.9.2",
@ -110,7 +112,7 @@
"asar": "^0.14.0",
"bower": "^1.8.2",
"chai": "^4.1.2",
"electron": "2.0.1",
"electron": "2.0.8",
"electron-builder": "^20.13.5",
"electron-icon-maker": "0.0.3",
"eslint": "^4.14.0",
@ -127,7 +129,7 @@
"grunt-contrib-watch": "^1.0.0",
"grunt-exec": "^3.0.0",
"grunt-gitinfo": "^0.1.7",
"grunt-sass": "^2.0.0",
"grunt-sass": "^3.0.1",
"mocha": "^4.1.0",
"mocha-testcheck": "^1.0.0-rc.0",
"node-sass-import-once": "^1.2.0",

@ -208,6 +208,7 @@ message SyncMessage {
message Blocked {
repeated string numbers = 1;
repeated bytes groupIds = 2;
}
message Request {
@ -303,4 +304,5 @@ message GroupDetails {
optional bool active = 5 [default = true];
optional uint32 expireTimer = 6;
optional string color = 7;
optional bool blocked = 8;
}

@ -89,14 +89,14 @@
<hr>
<div class='spell-check-setting'>
<h3>{{ spellCheckHeader }}</h3>
<input type='checkbox' name='spell-check-setting' />
<input type='checkbox' name='spell-check-setting' id='spell-check-setting' />
<label for='spell-check-setting'>{{ spellCheckDescription }}</label>
</div>
<hr>
<div class='permissions-setting'>
<h3>{{ permissions }}</h3>
<div class='media-permissions'>
<input type='checkbox' name='media-permissions' />
<input type='checkbox' name='media-permissions' id='media-permissions' />
<label for='media-permissions'>{{ mediaPermissionsDescription }}</label>
</div>
</div>

@ -125,6 +125,14 @@
color: $color-light-90;
}
.module-expire-timer {
background-color: $color-white-07;
}
.module-expire-timer--incoming {
background-color: $color-light-45;
}
.module-quote--incoming {
background-color: $color-signal-blue-025;
border-left-color: $color-signal-blue;
@ -218,6 +226,22 @@
}
}
.module-embedded-contact__contact-name {
color: $color-white;
}
.module-embedded-contact__contact-method {
color: $color-white-07;
}
.module-embedded-contact__contact-name--incoming {
color: $color-light-90;
}
.module-embedded-contact__contact-method--incoming {
color: $color-light-60;
}
&.dark-theme {
// _modules
@ -270,7 +294,7 @@
}
.module-message__metadata__date--incoming {
color: $color-white;
color: $color-white-07;
}
.module-message__broken-image--incoming {
@ -286,6 +310,10 @@
color: $color-white;
}
.module-expire-timer--incoming {
background-color: $color-white-07;
}
.module-quote__primary__author {
color: $color-light-90;
}
@ -401,7 +429,11 @@
}
}
.module-embedded-contact__contact-method {
.module-embedded-contact__contact-name--incoming {
color: $color-white;
}
.module-embedded-contact__contact-method--incoming {
color: $color-white-07;
}
}

@ -293,21 +293,6 @@
border-bottom-right-radius: 16px;
}
.module-message__broken-image {
font-size: 11px;
line-height: 16px;
letter-spacing: 0.3px;
padding: 10px;
text-align: center;
text-transform: uppercase;
color: $color-light-90;
}
.module-message__broken-image--incoming {
color: $color-white;
}
.module-message__video-overlay__circle {
position: absolute;
top: 50%;
@ -331,23 +316,6 @@
@include color-svg('../images/play.svg', $color-signal-blue);
}
.module-message__broken-video-screenshot {
font-size: 11px;
line-height: 16px;
letter-spacing: 0.3px;
padding: 10px;
text-align: center;
text-transform: uppercase;
color: $color-light-90;
cursor: pointer;
}
.module-message__broken-video-screenshot--incoming {
color: $color-white;
}
.module-message__audio-attachment {
margin-top: 2px;
}

@ -28,6 +28,7 @@ const contact = {
authorColor="green"
direction="incoming"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -36,6 +37,7 @@ const contact = {
direction="outgoing"
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -45,6 +47,7 @@ const contact = {
direction="incoming"
collapseMetadata
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -54,6 +57,7 @@ const contact = {
collapseMetadata
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -85,11 +89,13 @@ const contact = {
authorColor="green"
direction="incoming"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}/></li>
<li><Message
direction="outgoing"
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}/></li>
</util.ConversationContext>;
```
@ -123,6 +129,7 @@ const contact = {
authorAvatarPath={util.gifObjectUrl}
direction="incoming"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -134,6 +141,7 @@ const contact = {
conversationType="group"
collapseMetadata
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -144,6 +152,7 @@ const contact = {
authorName="Mr. Fire"
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -176,6 +185,7 @@ const contact = {
authorColor="green"
direction="incoming"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -184,6 +194,7 @@ const contact = {
direction="outgoing"
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -193,6 +204,7 @@ const contact = {
direction="incoming"
collapseMetadata
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -202,6 +214,7 @@ const contact = {
collapseMetadata
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -232,6 +245,7 @@ const contact = {
authorColor="green"
direction="incoming"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -240,6 +254,7 @@ const contact = {
direction="outgoing"
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -249,6 +264,7 @@ const contact = {
direction="incoming"
collapseMetadata
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -258,6 +274,7 @@ const contact = {
collapseMetadata
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -290,6 +307,7 @@ const contact = {
authorColor="green"
direction="incoming"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -298,6 +316,7 @@ const contact = {
direction="outgoing"
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -307,6 +326,7 @@ const contact = {
direction="incoming"
collapseMetadata
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -316,6 +336,7 @@ const contact = {
collapseMetadata
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -343,6 +364,7 @@ const contact = {
authorColor="green"
direction="incoming"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -351,6 +373,7 @@ const contact = {
direction="outgoing"
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -360,6 +383,7 @@ const contact = {
direction="incoming"
collapseMetadata
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -369,6 +393,7 @@ const contact = {
collapseMetadata
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -385,6 +410,7 @@ const contact = {};
authorColor="green"
direction="incoming"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -393,6 +419,7 @@ const contact = {};
direction="outgoing"
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -402,6 +429,7 @@ const contact = {};
direction="incoming"
collapseMetadata
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -411,6 +439,7 @@ const contact = {};
collapseMetadata
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contact}
/>
</li>
@ -461,6 +490,7 @@ const contactWithoutAccount = {
authorColor="green"
direction="incoming"
i18n={util.i18n}
timestamp={Date.now()}
contact={contactWithAccount}
/>
</li>
@ -470,6 +500,7 @@ const contactWithoutAccount = {
direction="outgoing"
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contactWithAccount}
/>
</li>
@ -479,6 +510,7 @@ const contactWithoutAccount = {
authorColor="green"
direction="incoming"
i18n={util.i18n}
timestamp={Date.now()}
contact={contactWithAccount}
/>
</li>
@ -488,6 +520,7 @@ const contactWithoutAccount = {
direction="outgoing"
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contactWithAccount}
/>
</li>
@ -498,6 +531,7 @@ const contactWithoutAccount = {
direction="incoming"
collapseMetadata
i18n={util.i18n}
timestamp={Date.now()}
contact={contactWithoutAccount}
/>
</li>
@ -508,6 +542,7 @@ const contactWithoutAccount = {
collapseMetadata
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contactWithoutAccount}
/>
</li>
@ -518,6 +553,7 @@ const contactWithoutAccount = {
direction="incoming"
collapseMetadata
i18n={util.i18n}
timestamp={Date.now()}
contact={contactWithoutAccount}
/>
</li>
@ -528,6 +564,7 @@ const contactWithoutAccount = {
collapseMetadata
status="delivered"
i18n={util.i18n}
timestamp={Date.now()}
contact={contactWithoutAccount}
/>
</li>

@ -1,13 +1,14 @@
### Countdown at different rates
```jsx
<util.ConversationContext theme={util.theme}>
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
<Message
authorColor="cyan"
direction="incoming"
text="10 second timer"
i18n={util.i18n}
timestamp={Date.now() + 10 * 1000}
expirationLength={10 * 1000}
expirationTimestamp={Date.now() + 10 * 1000}
/>
@ -18,6 +19,7 @@
authorColor="cyan"
text="30 second timer"
i18n={util.i18n}
timestamp={Date.now() + 30 * 1000}
expirationLength={30 * 1000}
expirationTimestamp={Date.now() + 30 * 1000}
/>
@ -28,6 +30,7 @@
direction="incoming"
text="1 minute timer"
i18n={util.i18n}
timestamp={Date.now() + 55 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 55 * 1000}
/>
@ -38,6 +41,7 @@
direction="incoming"
text="5 minute timer"
i18n={util.i18n}
timestamp={Date.now() + 5 * 60 * 1000}
expirationLength={5 * 60 * 1000}
expirationTimestamp={Date.now() + 5 * 60 * 1000}
/>
@ -48,13 +52,14 @@
### Timer calculations
```jsx
<util.ConversationContext theme={util.theme}>
<util.ConversationContext theme={util.theme} ios={util.ios}>
<li>
<Message
authorColor="cyan"
direction="incoming"
text="Full timer"
i18n={util.i18n}
timestamp={Date.now() + 60 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 60 * 1000}
/>
@ -65,6 +70,7 @@
status="delivered"
text="Full timer"
i18n={util.i18n}
timestamp={Date.now() + 60 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 60 * 1000}
/>
@ -75,6 +81,7 @@
direction="incoming"
text="55 timer"
i18n={util.i18n}
timestamp={Date.now() + 55 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 55 * 1000}
/>
@ -85,6 +92,7 @@
status="delivered"
text="55 timer"
i18n={util.i18n}
timestamp={Date.now() + 55 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 55 * 1000}
/>
@ -95,6 +103,7 @@
direction="incoming"
text="30 timer"
i18n={util.i18n}
timestamp={Date.now() + 30 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 30 * 1000}
/>
@ -105,6 +114,7 @@
status="delivered"
text="30 timer"
i18n={util.i18n}
timestamp={Date.now() + 30 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 30 * 1000}
/>
@ -115,6 +125,7 @@
direction="incoming"
text="5 timer"
i18n={util.i18n}
timestamp={Date.now() + 5 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 5 * 1000}
/>
@ -125,6 +136,7 @@
status="delivered"
text="5 timer"
i18n={util.i18n}
timestamp={Date.now() + 5 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 5 * 1000}
/>
@ -135,6 +147,7 @@
direction="incoming"
text="Expired timer"
i18n={util.i18n}
timestamp={Date.now()}
expirationLength={60 * 1000}
expirationTimestamp={Date.now()}
/>
@ -145,6 +158,7 @@
status="delivered"
text="Expired timer"
i18n={util.i18n}
timestamp={Date.now()}
expirationLength={60 * 1000}
expirationTimestamp={Date.now()}
/>
@ -155,6 +169,7 @@
direction="incoming"
text="Expiration is too far away"
i18n={util.i18n}
timestamp={Date.now() + 120 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 120 * 1000}
/>
@ -165,6 +180,7 @@
status="delivered"
text="Expiration is too far away"
i18n={util.i18n}
timestamp={Date.now() + 120 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() + 120 * 1000}
/>
@ -175,6 +191,7 @@
direction="incoming"
text="Already expired"
i18n={util.i18n}
timestamp={Date.now() - 20 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() - 20 * 1000}
/>
@ -185,6 +202,7 @@
status="delivered"
text="Already expired"
i18n={util.i18n}
timestamp={Date.now() - 20 * 1000}
expirationLength={60 * 1000}
expirationTimestamp={Date.now() - 20 * 1000}
/>

@ -126,12 +126,6 @@
{
type: 'remove',
isMe: true,
contacts: [
{
phoneNumber: '(202) 555-1000',
profileName: 'Mr. Fire',
},
],
},
]}
i18n={util.i18n}

@ -71,14 +71,14 @@ export class GroupNotification extends React.Component<Props> {
/>
);
case 'remove':
if (!contacts || !contacts.length) {
throw new Error('Group update is missing contacts');
}
if (isMe) {
return i18n('youLeftTheGroup');
}
if (!contacts || !contacts.length) {
throw new Error('Group update is missing contacts');
}
return (
<Intl
i18n={i18n}

File diff suppressed because it is too large Load Diff

@ -81,6 +81,7 @@ export interface Props {
referencedMessageNotFound: boolean;
};
authorAvatarPath?: string;
isExpired: boolean;
expirationLength?: number;
expirationTimestamp?: number;
onClickAttachment?: () => void;
@ -127,6 +128,12 @@ function isAudio(attachment?: Attachment) {
);
}
function canDisplayImage(attachment?: Attachment) {
const { height, width } = attachment || { height: 0, width: 0 };
return height > 0 && height <= 4096 && width > 0 && width <= 4096;
}
function getInitial(name: string): string {
return name.trim()[0] || '#';
}
@ -211,15 +218,22 @@ export class Message extends React.Component<Props, State> {
}
}
public componentDidUpdate() {
this.checkExpired();
}
public checkExpired() {
const now = Date.now();
const { expirationTimestamp, expirationLength } = this.props;
const { isExpired, expirationTimestamp, expirationLength } = this.props;
if (!expirationTimestamp || !expirationLength) {
return;
}
if (this.expiredTimeout) {
return;
}
if (now >= expirationTimestamp) {
if (isExpired || now >= expirationTimestamp) {
this.setState({
expiring: true,
});
@ -259,8 +273,10 @@ export class Message extends React.Component<Props, State> {
return null;
}
const canDisplayAttachment = canDisplayImage(attachment);
const withImageNoCaption = Boolean(
!text &&
canDisplayAttachment &&
!imageBroken &&
((isImage(attachment) && hasImage(attachment)) ||
(isVideo(attachment) && hasVideoScreenshot(attachment)))
@ -374,23 +390,11 @@ export class Message extends React.Component<Props, State> {
const withContentBelow = withCaption || !collapseMetadata;
const withContentAbove =
quote || (conversationType === 'group' && direction === 'incoming');
const displayImage = canDisplayImage(attachment);
if (isImage(attachment)) {
if (imageBroken || !attachment.url) {
return (
<div
className={classNames(
'module-message__broken-image',
`module-message__broken-image--${direction}`
)}
>
{i18n('imageFailedToLoad')}
</div>
);
}
if (isImage(attachment) && displayImage && !imageBroken && attachment.url) {
// Calculating height to prevent reflow when image loads
const height = Math.max(MINIMUM_IMG_HEIGHT, attachment.height || 0);
const imageHeight = Math.max(MINIMUM_IMG_HEIGHT, attachment.height || 0);
return (
<div
@ -409,7 +413,7 @@ export class Message extends React.Component<Props, State> {
<img
onError={this.handleImageErrorBound}
className="module-message__img-attachment"
height={Math.min(MAXIMUM_IMG_HEIGHT, height)}
height={Math.min(MAXIMUM_IMG_HEIGHT, imageHeight)}
src={attachment.url}
alt={i18n('imageAttachmentAlt')}
/>
@ -429,25 +433,19 @@ export class Message extends React.Component<Props, State> {
) : null}
</div>
);
} else if (isVideo(attachment)) {
} else if (
isVideo(attachment) &&
displayImage &&
!imageBroken &&
attachment.screenshot &&
attachment.screenshot.url
) {
const { screenshot } = attachment;
if (imageBroken || !screenshot || !screenshot.url) {
return (
<div
role="button"
onClick={onClickAttachment}
className={classNames(
'module-message__broken-video-screenshot',
`module-message__broken-video-screenshot--${direction}`
)}
>
{i18n('videoScreenshotFailedToLoad')}
</div>
);
}
// Calculating height to prevent reflow when image loads
const height = Math.max(MINIMUM_IMG_HEIGHT, screenshot.height || 0);
const imageHeight = Math.max(
MINIMUM_IMG_HEIGHT,
attachment.screenshot.height || 0
);
return (
<div
@ -467,7 +465,7 @@ export class Message extends React.Component<Props, State> {
onError={this.handleImageErrorBound}
className="module-message__img-attachment"
alt={i18n('videoAttachmentAlt')}
height={Math.min(MAXIMUM_IMG_HEIGHT, height)}
height={Math.min(MAXIMUM_IMG_HEIGHT, imageHeight)}
src={screenshot.url}
/>
<div

@ -1066,6 +1066,8 @@
url: util.gifObjectUrl,
fileName: 'pi.gif',
contentType: 'image/gif',
width: 320,
height: 240,
}}
text="About six"
i18n={util.i18n}
@ -1085,6 +1087,8 @@
url: util.gifObjectUrl,
fileName: 'pi.gif',
contentType: 'image/gif',
width: 320,
height: 240,
}}
authorColor="green"
text="About six"
@ -1112,6 +1116,8 @@
url: util.gifObjectUrl,
fileName: 'pi.gif',
contentType: 'image/gif',
width: 320,
height: 240,
}}
i18n={util.i18n}
quote={{
@ -1130,6 +1136,8 @@
url: util.gifObjectUrl,
fileName: 'pi.gif',
contentType: 'image/gif',
width: 320,
height: 240,
}}
authorColor="green"
i18n={util.i18n}
@ -1156,6 +1164,8 @@
url: util.portraitYellowObjectUrl,
fileName: 'pi.gif',
contentType: 'image/gif',
width: 20,
height: 200,
}}
i18n={util.i18n}
quote={{
@ -1174,6 +1184,8 @@
url: util.portraitYellowObjectUrl,
fileName: 'pi.gif',
contentType: 'image/gif',
width: 20,
height: 200,
}}
authorColor="green"
i18n={util.i18n}
@ -1200,9 +1212,13 @@
screenshot: {
url: util.pngObjectUrl,
contentType: 'image/png',
width: 800,
height: 1200,
},
fileName: 'freezing_bubble.mp4',
contentType: 'video/mp4',
width: 800,
height: 1200,
}}
i18n={util.i18n}
quote={{
@ -1221,9 +1237,13 @@
screenshot: {
url: util.pngObjectUrl,
contentType: 'image/png',
width: 800,
height: 1200,
},
fileName: 'freezing_bubble.mp4',
contentType: 'video/mp4',
width: 800,
height: 1200,
}}
authorColor="green"
i18n={util.i18n}

@ -1,4 +1 @@
/**
* @prettier
*/
export type AttachmentType = 'media' | 'documents';

@ -1,6 +1,3 @@
/**
* @prettier
*/
import { AttachmentType } from './AttachmentType';
import { Message } from './Message';

@ -0,0 +1,29 @@
// tslint:disable no-console
import { join } from 'path';
import { fromPairs, groupBy, map } from 'lodash';
import { ExceptionType } from './types';
import { loadJSON } from './util';
const exceptionsPath = join(__dirname, 'exceptions.json');
const exceptions: Array<ExceptionType> = loadJSON(exceptionsPath);
const byRule = groupBy(exceptions, 'rule');
const byRuleThenByCategory = fromPairs(
map(byRule, (list, ruleName) => {
const byCategory = groupBy(list, 'reasonCategory');
return [
ruleName,
fromPairs(
map(byCategory, (innerList, categoryName) => {
return [categoryName, innerList.length];
})
),
];
})
);
console.log(JSON.stringify(byRuleThenByCategory, null, ' '));

File diff suppressed because it is too large Load Diff

@ -0,0 +1,265 @@
// tslint:disable no-console
import { readFileSync } from 'fs';
import { join, relative } from 'path';
// @ts-ignore
import glob from 'glob';
import { forEach, some, values } from 'lodash';
import { ExceptionType, REASONS, RuleType } from './types';
import { ENCODING, loadJSON } from './util';
const ALL_REASONS = REASONS.join('|');
const now = new Date();
function getExceptionKey(exception: any) {
return `${exception.rule}-${exception.path}-${exception.lineNumber}`;
}
function createLookup(list: Array<any>) {
const lookup = Object.create(null);
forEach(list, exception => {
const key = getExceptionKey(exception);
if (lookup[key]) {
throw new Error(`Duplicate exception found for key ${key}`);
}
lookup[key] = exception;
});
return lookup;
}
const rulesPath = join(__dirname, 'rules.json');
const exceptionsPath = join(__dirname, 'exceptions.json');
const basePath = join(__dirname, '../../..');
const searchPattern = join(basePath, '**/*.{js,ts,tsx}');
const rules: Array<RuleType> = loadJSON(rulesPath);
const exceptions: Array<ExceptionType> = loadJSON(exceptionsPath);
const exceptionsLookup = createLookup(exceptions);
let scannedCount = 0;
const allSourceFiles = glob.sync(searchPattern, { nodir: true });
const results: Array<ExceptionType> = [];
const excludedFiles = [
// Generated files
'^js/components.js',
'^js/libtextsecure.js',
'^js/util_worker.js',
'^libtextsecure/components.js',
'^libtextsecure/test/test.js',
'^test/test.js',
// From libsignal-protocol-javascript project
'^js/libsignal-protocol-worker.js',
'^libtextsecure/libsignal-protocol.js',
// Copied from dependency
'^js/Mp3LameEncoder.min.js',
// Test files
'^libtextsecure/test/*',
'^test/*',
// Modules used only in test/development scenarios
'^node_modules/@types/*',
'^node_modules/ajv/*',
'^node_modules/amdefine/*',
'^node_modules/anymatch/*',
'^node_modules/asn1\\.js/*',
'^node_modules/autoprefixer/*',
'^node_modules/babel*',
'^node_modules/bluebird/*',
'^node_modules/body-parser/*',
'^node_modules/bower/*',
'^node_modules/buble/*',
'^node_modules/chai/*',
'^node_modules/cli-table2/*',
'^node_modules/codemirror/*',
'^node_modules/coffee-script/*',
'^node_modules/compression/*',
'^node_modules/degenerator/*',
'^node_modules/detect-port-alt/*',
'^node_modules/electron-builder/*',
'^node_modules/electron-icon-maker/*',
'^node_modules/electron-osx-sign/*',
'^node_modules/electron-publish/*',
'^node_modules/escodegen/*',
'^node_modules/eslint*',
'^node_modules/esprima/*',
'^node_modules/express/*',
'^node_modules/extract-zip/*',
'^node_modules/finalhandler/*',
'^node_modules/fsevents/*',
'^node_modules/globule/*',
'^node_modules/grunt*',
'^node_modules/handle-thing/*',
'^node_modules/har-validator/*',
'^node_modules/highlight\\.js/*',
'^node_modules/hpack\\.js/*',
'^node_modules/http-proxy-middlewar/*',
'^node_modules/icss-utils/*',
'^node_modules/intl-tel-input/examples/*',
'^node_modules/istanbul*',
'^node_modules/jimp/*',
'^node_modules/jquery/*',
'^node_modules/jss/*',
'^node_modules/jss-global/*',
'^node_modules/livereload-js/*',
'^node_modules/lolex/*',
'^node_modules/magic-string/*',
'^node_modules/mocha/*',
'^node_modules/minimatch/*',
'^node_modules/nise/*',
'^node_modules/node-sass-import-once/*',
'^node_modules/node-sass/*',
'^node_modules/nsp/*',
'^node_modules/nyc/*',
'^node_modules/phantomjs-prebuilt/*',
'^node_modules/postcss*',
'^node_modules/preserve/*',
'^node_modules/prettier/*',
'^node_modules/protobufjs/cli/*',
'^node_modules/ramda/*',
'^node_modules/react-docgen/*',
'^node_modules/react-error-overlay/*',
'^node_modules/react-styleguidist/*',
'^node_modules/recast/*',
'^node_modules/reduce-css-calc/*',
'^node_modules/resolve/*',
'^node_modules/sass-graph/*',
'^node_modules/scss-tokenizer/*',
'^node_modules/send/*',
'^node_modules/serve-index/*',
'^node_modules/sinon/*',
'^node_modules/snapdragon-util/*',
'^node_modules/snapdragon/*',
'^node_modules/sockjs-client/*',
'^node_modules/spectron/*',
'^node_modules/style-loader/*',
'^node_modules/svgo/*',
'^node_modules/testcheck/*',
'^node_modules/text-encoding/*',
'^node_modules/tinycolor2/*',
'^node_modules/to-ast/*',
'^node_modules/trough/*',
'^node_modules/ts-loader/*',
'^node_modules/tslint*',
'^node_modules/tweetnacl/*',
'^node_modules/typescript/*',
'^node_modules/uglify-es/*',
'^node_modules/uglify-js/*',
'^node_modules/use/*',
'^node_modules/vary/*',
'^node_modules/vm-browserify/*',
'^node_modules/webdriverio/*',
'^node_modules/webpack*',
'^node_modules/xmldom/*',
'^node_modules/xml-parse-from-string/*',
];
function setupRules(allRules: Array<RuleType>) {
forEach(allRules, (rule, index) => {
if (!rule.name) {
throw new Error(`Rule at index ${index} is missing a name`);
}
if (!rule.expression) {
throw new Error(`Rule '${rule.name}' is missing an expression`);
}
rule.regex = new RegExp(rule.expression, 'g');
});
}
setupRules(rules);
forEach(allSourceFiles, file => {
const relativePath = relative(basePath, file).replace(/\\/g, '/');
if (
some(excludedFiles, excluded => {
const regex = new RegExp(excluded);
return regex.test(relativePath);
})
) {
return;
}
scannedCount += 1;
const fileContents = readFileSync(file, ENCODING);
const lines = fileContents.split('\n');
forEach(rules, (rule: RuleType) => {
const excludedModules = rule.excludedModules || [];
if (some(excludedModules, module => relativePath.startsWith(module))) {
return;
}
forEach(lines, (rawLine, lineIndex) => {
const line = rawLine.replace(/\r/g, '');
if (!rule.regex.test(line)) {
return;
}
const path = relativePath;
const lineNumber = lineIndex + 1;
const exceptionKey = getExceptionKey({
rule: rule.name,
path: relativePath,
lineNumber,
});
const exception = exceptionsLookup[exceptionKey];
if (exception && (!exception.line || exception.line === line)) {
delete exceptionsLookup[exceptionKey];
return;
}
results.push({
rule: rule.name,
path,
line: line.length < 300 ? line : undefined,
lineNumber,
reasonCategory: ALL_REASONS,
updated: now.toJSON(),
reasonDetail: '<optional>',
});
});
});
});
const unusedExceptions = values(exceptionsLookup);
console.log(
`${scannedCount} files scanned.`,
`${results.length} questionable lines,`,
`${unusedExceptions.length} unused exceptions,`,
`${exceptions.length} total exceptions.`
);
if (results.length === 0 && unusedExceptions.length === 0) {
process.exit();
}
console.log();
console.log('Questionable lines:');
console.log(JSON.stringify(results, null, ' '));
if (unusedExceptions.length) {
console.log();
console.log('Unused exceptions!');
console.log(JSON.stringify(unusedExceptions, null, ' '));
}
process.exit(1);

@ -0,0 +1,173 @@
[
{
"name": "eval",
"expression": "\\beval\\(",
"reason": "Arbitrary code execution"
},
{
"name": "DOM-innerHTML",
"expression": "\\binnerHTML\\b",
"reason": "Potential XSS"
},
{
"name": "DOM-outerHTML",
"expression": "\\bouterHTML\\b",
"reason": "Potential XSS"
},
{
"name": "DOM-document.write(",
"expression": "\\bdocument.write(ln)?\\(",
"reason": "Potential XSS"
},
{
"name": "jQuery-$(",
"expression": "\\$\\(",
"reason": "Potential XSS",
"excludedModules": ["node_modules/prelude-ls"]
},
{
"name": "jQuery-html(",
"expression": "\\bhtml\\(",
"reason": "Potential XSS"
},
{
"name": "jQuery-append(",
"expression": "\\bappend\\(",
"reason": "Potential XSS",
"excludedModules": [
"components/bytebuffer",
"components/protobuf",
"node_modules/google-libphonenumber",
"node_modules/handlebars"
]
},
{
"name": "jQuery-appendTo(",
"expression": "\\bappendTo\\(",
"reason": "Potential XSS"
},
{
"name": "jQuery-insertAfter(",
"expression": "\\binsertAfter\\(",
"reason": "Potential XSS"
},
{
"name": "jQuery-insertBefore(",
"expression": "\\binsertBefore\\(",
"reason": "Potential XSS",
"excludedModules": ["node_modules/react-dom"]
},
{
"name": "jQuery-prepend(",
"expression": "\\bprepend\\(",
"reason": "Potential XSS",
"excludedModules": ["components/bytebuffer", "node_modules/handlebars"]
},
{
"name": "jQuery-prependTo(",
"expression": "\\bprependTo\\(",
"reason": "Potential XSS"
},
{
"name": "jQuery-wrap(",
"expression": "\\bwrap\\(",
"reason": "Potential XSS",
"excludedModules": [
"components/bytebuffer",
"components/protobuf",
"node_modules/handlebars",
"node_modules/lodash"
]
},
{
"name": "jQuery-wrapInner(",
"expression": "\\bwrapInner\\(",
"reason": "Potential XSS"
},
{
"name": "jQuery-wrapAll(",
"expression": "\\bwrapAll\\(",
"reason": "Potential XSS"
},
{
"name": "jQuery-before(",
"expression": "\\bbefore\\(",
"reason": "Potential XSS"
},
{
"name": "jQuery-after(",
"expression": "\\bafter\\(",
"reason": "Potential XSS"
},
{
"name": "jQuery-globalEval(",
"expression": "\\bglobalEval\\(",
"reason": "Arbitrary code execution"
},
{
"name": "jQuery-getScript(",
"expression": "\\bgetScript\\(",
"reason": "Arbitrary code execution"
},
{
"name": "jQuery-load(",
"expression": "\\bload\\(",
"reason": "Arbitrary code execution"
},
{
"name": "React-ref",
"expression": "\\bref(\\s)*=\\b",
"reason": "Potential XSS",
"excludedModules": [
"node_modules/react-dom",
"node_modules/tslint-microsoft-contrib",
"node_modules/react-error-overlay",
"node_modules/react-styleguidist"
]
},
{
"name": "React-createRef",
"expression": "\\bcreateRef\\(",
"reason": "Potential XSS",
"excludedModules": [
"node_modules/react-dom",
"node_modules/tslint-microsoft-contrib",
"node_modules/react-error-overlay",
"node_modules/react-styleguidist"
]
},
{
"name": "React-findDOMNode",
"expression": "\\bfindDOMNode\\(",
"reason": "Potential XSS",
"excludedModules": [
"node_modules/react-dom",
"node_modules/tslint-microsoft-contrib",
"node_modules/react-error-overlay",
"node_modules/react-styleguidist"
]
},
{
"name": "React-dangerouslySetInnerHTML",
"expression": "\\bdangerouslySetInnerHTML\\b",
"reason": "Potential XSS",
"excludedModules": [
"node_modules/react-dom",
"node_modules/tslint-microsoft-contrib",
"node_modules/react-error-overlay",
"node_modules/react-styleguidist"
]
},
{
"name": "fbjs-createNodesFromMarkup",
"expression": "\\bcreateNodesFromMarkup\\b",
"reason": "Potential XSS, pipes input to innerHTML",
"excludedModules": ["node_modules/react-dom", "node_modules/fbjs"]
},
{
"name": "thenify-multiArgs",
"expression": "\\bmultiArgs\\b",
"reason": "Potential arbitrary code execution, piped to eval",
"excludedModules": ["node_modules/thenify"]
}
]

@ -0,0 +1,14 @@
// tslint:disable no-console
import { join } from 'path';
import { writeFileSync } from 'fs';
import { ExceptionType } from './types';
import { loadJSON, sortExceptions } from './util';
const exceptionsPath = join(__dirname, 'exceptions.json');
const exceptions: Array<ExceptionType> = loadJSON(exceptionsPath);
const sorted = sortExceptions(exceptions);
writeFileSync(exceptionsPath, JSON.stringify(sorted, null, ' '));

@ -0,0 +1,63 @@
// Tool requirements:
// - Feed it a set of regular expressions with descriptions as to what the risks are
// - Feed it also a set of exceptions
// - It would tell us if there were any new matches that didn't already have exceptions
//
// Rules:
// {
// "name": "rule-name",
// "expression": "^regex-as-string$",
// "reason": "Reason that this expression is dangerous"
// }
//
// Categories of reasons - low to high risk:
// "falseMatch"
// "testCode"
// "exampleCode"
// "otherUtilityCode"
// "regexMatchedSafeCode"
// "notExercisedByOurApp"
// "ruleNeeded"
// "usageTrusted"
//
// Exceptions:
// [{
// "rule": "rule-name",
// "path": "path/to/filename.js",
// "lineNumber": 45,
// "reasonCategory": "<category from list above>",
// "updated": "2018-09-08T00:21:13.180Z",
// "reasonDetail": "<Optional additional information about why this is okay>"
// }]
//
// When the tool finds issues it outputs them in exception format to make it easy to add
// to the exceptions.json file
export const REASONS = [
'falseMatch',
'testCode',
'exampleCode',
'otherUtilityCode',
'regexMatchedSafeCode',
'notExercisedByOurApp',
'ruleNeeded',
'usageTrusted',
];
export type RuleType = {
name: string;
expression: string | null;
reason: string;
regex: RegExp;
excludedModules: Array<string> | null;
};
export type ExceptionType = {
rule: string;
path: string;
line?: string;
lineNumber: number;
reasonCategory: string;
updated: string;
reasonDetail: string;
};

@ -0,0 +1,24 @@
// tslint:disable no-console
import { readFileSync } from 'fs';
import { orderBy } from 'lodash';
import { ExceptionType } from './types';
export const ENCODING = 'utf8';
export function loadJSON(target: string) {
try {
const contents = readFileSync(target, ENCODING);
return JSON.parse(contents);
} catch (error) {
console.log(`Error loading JSON from ${target}: ${error.stack}`);
throw error;
}
}
export function sortExceptions(exceptions: Array<ExceptionType>) {
return orderBy(exceptions, ['path', 'lineNumber', 'rule']);
}

@ -114,9 +114,9 @@
version "4.14.106"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.106.tgz#6093e9a02aa567ddecfe9afadca89e53e5dce4dd"
"@types/long@^3.0.32":
version "3.0.32"
resolved "https://registry.yarnpkg.com/@types/long/-/long-3.0.32.tgz#f4e5af31e9e9b196d8e5fca8a5e2e20aa3d60b69"
"@types/long@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef"
"@types/mocha@^5.0.0":
version "5.0.0"
@ -126,14 +126,14 @@
version "9.6.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.1.tgz#e2d374ef15b315b48e7efc308fa1a7cd51faa06c"
"@types/node@^10.1.0":
version "10.10.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.10.1.tgz#d5c96ca246a418404914d180b7fdd625ad18eca6"
"@types/node@^8.0.24":
version "8.9.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.9.4.tgz#dfd327582a06c114eb6e0441fa3d6fab35edad48"
"@types/node@^8.9.4":
version "8.10.12"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.12.tgz#dcb66f6de39074a296534bd1a256a3c6a1c8f5b5"
"@types/qs@^6.5.1":
version "6.5.1"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.5.1.tgz#a38f69c62528d56ba7bd1f91335a8004988d72f7"
@ -645,6 +645,10 @@ aws4@^1.2.1, aws4@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
aws4@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
babel-code-frame@6.26.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
@ -1529,7 +1533,7 @@ colors@~0.6.0-1:
version "0.6.2"
resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc"
combined-stream@1.0.6:
combined-stream@1.0.6, combined-stream@~1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
dependencies:
@ -1647,15 +1651,6 @@ concat-stream@^1.5.0:
readable-stream "^2.2.2"
typedarray "^0.0.6"
conf@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/conf/-/conf-1.1.1.tgz#238d0a3090ac4916ed2d40c7e81d7a11667bc7ba"
dependencies:
dot-prop "^4.1.0"
env-paths "^1.0.0"
make-dir "^1.0.0"
pkg-up "^2.0.0"
config@^1.28.1:
version "1.28.1"
resolved "https://registry.yarnpkg.com/config/-/config-1.28.1.tgz#7625d2a1e4c90f131d8a73347982d93c3873282d"
@ -2343,13 +2338,6 @@ duplexify@^3.4.2, duplexify@^3.5.3:
readable-stream "^2.0.0"
stream-shift "^1.0.0"
each-async@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/each-async/-/each-async-1.1.1.tgz#dee5229bdf0ab6ba2012a395e1b869abf8813473"
dependencies:
onetime "^1.0.0"
set-immediate-shim "^1.0.0"
ecc-jsbn@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
@ -2423,12 +2411,6 @@ electron-chromedriver@~1.8.0:
electron-download "^4.1.0"
extract-zip "^1.6.5"
electron-config@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/electron-config/-/electron-config-1.0.0.tgz#069d044cc794f04784ae72f12916725d3c8c39af"
dependencies:
conf "^1.0.0"
electron-download-tf@4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/electron-download-tf/-/electron-download-tf-4.3.4.tgz#b03740b2885aa2ad3f8784fae74df427f66d5165"
@ -2538,9 +2520,9 @@ electron-updater@^2.21.10:
semver "^5.5.0"
source-map-support "^0.5.5"
electron@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/electron/-/electron-2.0.1.tgz#d9defcc187862143b9027378be78490eddbfabf4"
electron@2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/electron/-/electron-2.0.8.tgz#6ec7113b356e09cc9899797e0d41ebff8163e962"
dependencies:
"@types/node" "^8.0.24"
electron-download "^3.0.1"
@ -2989,6 +2971,10 @@ extend@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4"
extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
external-editor@^2.0.4:
version "2.1.0"
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.1.0.tgz#3d026a21b7f95b5726387d4200ac160d372c3b48"
@ -3257,7 +3243,7 @@ forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
form-data@^2.3.2:
form-data@^2.3.2, form-data@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
dependencies:
@ -3802,13 +3788,9 @@ grunt-legacy-util@~1.0.0:
underscore.string "~3.2.3"
which "~1.2.1"
grunt-sass@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/grunt-sass/-/grunt-sass-2.0.0.tgz#9074cf9d7b4592e20f7788caa727b8f9aa06b60a"
dependencies:
each-async "^1.0.0"
node-sass "^4.0.0"
object-assign "^4.0.1"
grunt-sass@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/grunt-sass/-/grunt-sass-3.0.1.tgz#2760207d7b78db84429d9fa77d22289a6fc903a0"
grunt@^1.0.1:
version "1.0.1"
@ -3873,6 +3855,13 @@ har-validator@~5.0.3:
ajv "^5.1.0"
har-schema "^2.0.0"
har-validator@~5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29"
dependencies:
ajv "^5.3.0"
har-schema "^2.0.0"
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@ -4756,8 +4745,8 @@ jquery@^3.3.1:
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca"
js-base64@^2.1.8:
version "2.1.9"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce"
version "2.4.9"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.9.tgz#748911fb04f48a60c4771b375cac45a80df11c03"
js-base64@^2.1.9:
version "2.4.3"
@ -5112,8 +5101,8 @@ lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
lodash.mergewith@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
lodash.uniq@^4.5.0:
version "4.5.0"
@ -5382,6 +5371,10 @@ mime-db@~1.30.0:
version "1.30.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
mime-db@~1.36.0:
version "1.36.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397"
mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.7:
version "2.1.15"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
@ -5400,6 +5393,12 @@ mime-types@~2.1.18:
dependencies:
mime-db "~1.33.0"
mime-types@~2.1.19:
version "2.1.20"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19"
dependencies:
mime-db "~1.36.0"
mime@1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
@ -5606,7 +5605,7 @@ mz@^2.3.1:
object-assign "^4.0.1"
thenify-all "^1.0.0"
nan@^2.0.0, nan@^2.3.2, nan@^2.3.3:
nan@^2.0.0, nan@^2.3.3:
version "2.6.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
@ -5692,19 +5691,18 @@ node-forge@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300"
node-gyp@^3.3.1:
version "3.6.1"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.1.tgz#19561067ff185464aded478212681f47fd578cbc"
node-gyp@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
dependencies:
fstream "^1.0.0"
glob "^7.0.3"
graceful-fs "^4.1.2"
minimatch "^3.0.2"
mkdirp "^0.5.0"
nopt "2 || 3"
npmlog "0 || 1 || 2 || 3 || 4"
osenv "0"
request "2"
request "^2.87.0"
rimraf "2"
semver "~5.3.0"
tar "^2.0.0"
@ -5775,9 +5773,9 @@ node-sass-import-once@^1.2.0:
dependencies:
js-yaml "^3.2.7"
node-sass@^4.0.0:
version "4.5.2"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.5.2.tgz#4012fa2bd129b1d6365117e88d9da0500d99da64"
node-sass@^4.9.3:
version "4.9.3"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.3.tgz#f407cf3d66f78308bb1e346b24fa428703196224"
dependencies:
async-foreach "^0.1.3"
chalk "^1.1.1"
@ -5791,12 +5789,13 @@ node-sass@^4.0.0:
lodash.mergewith "^4.6.0"
meow "^3.7.0"
mkdirp "^0.5.1"
nan "^2.3.2"
node-gyp "^3.3.1"
nan "^2.10.0"
node-gyp "^3.8.0"
npmlog "^4.0.0"
request "^2.79.0"
sass-graph "^2.1.1"
request "2.87.0"
sass-graph "^2.2.4"
stdout-stream "^1.4.0"
"true-case-path" "^1.0.2"
nodesecurity-npm-utils@^6.0.0:
version "6.0.0"
@ -5878,7 +5877,7 @@ npm-run-path@^2.0.0:
dependencies:
path-key "^2.0.0"
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0:
"npmlog@0 || 1 || 2 || 3 || 4":
version "4.0.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.2.tgz#d03950e0e78ce1527ba26d2a7592e9348ac3e75f"
dependencies:
@ -5887,7 +5886,7 @@ npm-run-path@^2.0.0:
gauge "~2.7.1"
set-blocking "~2.0.0"
npmlog@^4.0.2:
npmlog@^4.0.0, npmlog@^4.0.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
dependencies:
@ -5966,6 +5965,10 @@ oauth-sign@~0.8.1, oauth-sign@~0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
oauth-sign@~0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -6025,10 +6028,6 @@ once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0:
dependencies:
wrappy "1"
onetime@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
onetime@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
@ -6428,12 +6427,6 @@ pkg-dir@^2.0.0:
dependencies:
find-up "^2.1.0"
pkg-up@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
dependencies:
find-up "^2.1.0"
pkginfo@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.0.tgz#349dbb7ffd38081fcadc0853df687f0c7744cd65"
@ -6808,9 +6801,9 @@ prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1:
loose-envify "^1.3.1"
object-assign "^4.1.1"
protobufjs@^6.8.6:
version "6.8.6"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.6.tgz#ce3cf4fff9625b62966c455fc4c15e4331a11ca2"
protobufjs@~6.8.6:
version "6.8.8"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c"
dependencies:
"@protobufjs/aspromise" "^1.1.2"
"@protobufjs/base64" "^1.1.2"
@ -6822,8 +6815,8 @@ protobufjs@^6.8.6:
"@protobufjs/path" "^1.1.2"
"@protobufjs/pool" "^1.1.0"
"@protobufjs/utf8" "^1.1.0"
"@types/long" "^3.0.32"
"@types/node" "^8.9.4"
"@types/long" "^4.0.0"
"@types/node" "^10.1.0"
long "^4.0.0"
proxy-addr@~2.0.3:
@ -6854,6 +6847,10 @@ pseudomap@^1.0.1, pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
psl@^1.1.24:
version "1.1.29"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
public-encrypt@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
@ -6919,6 +6916,10 @@ qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
query-string@^4.1.0:
version "4.3.4"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
@ -7475,7 +7476,7 @@ request-progress@^2.0.1:
dependencies:
throttleit "^1.0.0"
request@2, request@2.81.0, request@^2.45.0, request@^2.65.0, request@^2.79.0:
request@2.81.0, request@^2.45.0, request@^2.65.0, request@^2.79.0:
version "2.81.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
dependencies:
@ -7502,6 +7503,31 @@ request@2, request@2.81.0, request@^2.45.0, request@^2.65.0, request@^2.79.0:
tunnel-agent "^0.6.0"
uuid "^3.0.0"
request@2.87.0:
version "2.87.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.6.0"
caseless "~0.12.0"
combined-stream "~1.0.5"
extend "~3.0.1"
forever-agent "~0.6.1"
form-data "~2.3.1"
har-validator "~5.0.3"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
mime-types "~2.1.17"
oauth-sign "~0.8.2"
performance-now "^2.1.0"
qs "~6.5.1"
safe-buffer "^5.1.1"
tough-cookie "~2.3.3"
tunnel-agent "^0.6.0"
uuid "^3.1.0"
request@^2.81.0, request@~2.83.0:
version "2.83.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
@ -7529,6 +7555,31 @@ request@^2.81.0, request@~2.83.0:
tunnel-agent "^0.6.0"
uuid "^3.1.0"
request@^2.87.0:
version "2.88.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.8.0"
caseless "~0.12.0"
combined-stream "~1.0.6"
extend "~3.0.2"
forever-agent "~0.6.1"
form-data "~2.3.2"
har-validator "~5.1.0"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
mime-types "~2.1.19"
oauth-sign "~0.9.0"
performance-now "^2.1.0"
qs "~6.5.2"
safe-buffer "^5.1.2"
tough-cookie "~2.4.3"
tunnel-agent "^0.6.0"
uuid "^3.3.2"
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@ -7701,14 +7752,14 @@ sanitize-filename@^1.6.1:
dependencies:
truncate-utf8-bytes "^1.0.0"
sass-graph@^2.1.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.2.tgz#f4d6c95b546ea2a09d14176d0fc1a07ee2b48354"
sass-graph@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
dependencies:
glob "^7.0.0"
lodash "^4.0.0"
scss-tokenizer "^0.2.1"
yargs "^6.6.0"
scss-tokenizer "^0.2.3"
yargs "^7.0.0"
sax@>=0.6.0:
version "1.2.1"
@ -7725,9 +7776,9 @@ schema-utils@^0.4.2, schema-utils@^0.4.5:
ajv "^6.1.0"
ajv-keywords "^3.1.0"
scss-tokenizer@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.1.tgz#07c0cc577bb7ab4d08fd900185adbf4bc844141d"
scss-tokenizer@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
dependencies:
js-base64 "^2.1.8"
source-map "^0.4.2"
@ -7811,7 +7862,7 @@ set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
set-immediate-shim@^1.0.0, set-immediate-shim@^1.0.1:
set-immediate-shim@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
@ -8202,8 +8253,8 @@ statuses@1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
stdout-stream@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b"
version "1.4.1"
resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de"
dependencies:
readable-stream "^2.0.1"
@ -8665,6 +8716,13 @@ tough-cookie@~2.3.3:
dependencies:
punycode "^1.4.1"
tough-cookie@~2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
dependencies:
psl "^1.1.24"
punycode "^1.4.1"
"traverse@>=0.3.0 <0.4":
version "0.3.9"
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
@ -8689,6 +8747,12 @@ trough@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.1.tgz#a9fd8b0394b0ae8fff82e0633a0a36ccad5b5f86"
"true-case-path@^1.0.2":
version "1.0.3"
resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d"
dependencies:
glob "^7.1.2"
truncate-utf8-bytes@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b"
@ -9489,6 +9553,12 @@ yargs-parser@^4.2.0:
dependencies:
camelcase "^3.0.0"
yargs-parser@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
dependencies:
camelcase "^3.0.0"
yargs-parser@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
@ -9507,7 +9577,7 @@ yargs-parser@^9.0.2:
dependencies:
camelcase "^4.1.0"
yargs@6.6.0, yargs@^6.5.0, yargs@^6.6.0:
yargs@6.6.0, yargs@^6.5.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208"
dependencies:
@ -9559,6 +9629,24 @@ yargs@^11.0.0:
y18n "^3.2.1"
yargs-parser "^9.0.2"
yargs@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
dependencies:
camelcase "^3.0.0"
cliui "^3.2.0"
decamelize "^1.1.1"
get-caller-file "^1.0.1"
os-locale "^1.4.0"
read-pkg-up "^1.0.1"
require-directory "^2.1.1"
require-main-filename "^1.0.1"
set-blocking "^2.0.0"
string-width "^1.0.2"
which-module "^1.0.0"
y18n "^3.2.1"
yargs-parser "^5.0.0"
yargs@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c"

Loading…
Cancel
Save